Alexito's World

A world of coding πŸ’», by Alejandro Martinez

Fluent syntax extensions in Swift

In the early days of Swift, one thing I really enjoyed was the focus on properties (bye-bye ivars!) and the unification of stored and computed property syntax. It became very common to define properties that initialized some parts of your view automatically.

let label = UILabel()

But we all quickly realised that it was not enough. You often want to initialize that label with some defaults set, things like font, color, etc. Swift has a way to do it with immediately-executed closures like so:

let label: UILabel = {
  let label = UILabel()
  label.textAlignment = .center
  label.textColor = .black
  label.text = "Hello, World!"
  return label
}()

But that is very verbose. You have to repeat the name multiple times and the immediately-executed closure is weird for newcomers.

The community quickly figured out this need in the early days of Swift Evolution. You can find a bunch of forums posts about the topic through the years, some of them in early days of the mailing list!

Some of them focus on just adding flexibility to the initialiser syntax and others go beyond that to offer a general solution to these common topics of fluent syntax. The reality is that none of those proposals ever got far, so the language still doesn't offer a pleasant alternative.

But since the early days devxoul/Then was created and quickly became a standard in the community. I include it by default on all my projects. That's how useful it is! That said, I always missed some functionality and recently I started investigating the topic deeper, which prompted me to write this post and open source a new package.

Swift limitations

Accepting that the language doesn't offer the solution we need, let's explore how a library can solve it. It’s important to understand the limitations in the language that limit what a library can do.

Any being a non-nominal type

The first thing you encounter is the impossibility of extending non-nominal types. We rarely think about this distinction in the language, but for the functionality we want is a real blocker. What we would love to do is something like this:

extension Any {
  func then(...
}

But Any is a non-nominal type so we can't extend it! The best alternative to this is to make your own marker protocol (without requirements) and conform all the types you can to it! It's not pretty, but it gets the job done.

protocol Flowable {}
extension Array: Flowable {}
...

Note that by extending NSObject to conform to our protocol, we make all Apple frameworks have this new functionality. (if they are in Objective-C and use classes)

With that in place, we can add extension functions to our protocol when the conformant type is a... well, Any!

extension Flowable where Self: Any {
  func then...

It's a roundabout way of getting there, but it ends up working.

The lack of self rebinding

Also known as receiver closures from Kotlin. This functionality allows a closure definition to specify what the "receiver" object in the closure's scope is. The "receiver" is the object that will receive a method call if we don't prefix it with a target object. It's what self is in a method.

func methodInAClass() {
   callSomeMethodInThisClass() // self is implicit, is the receiver object.
}
...

methodWithClosure(SomeObject()) {
  // This closure could make `SomeObject` the receiver.
  callSomeMethodInSomeObject() // we don't need $0
}

The advantage of this is that you get less boilerplate by avoiding the repeated usage of $0, which is the same reason the language allows us to omit self in methods. It would be very useful to have, since the main purpose of some methods we want is to reduce boilerplate. But alas!

What Flow offers

Even with these limitations, we can still offer a lot of interesting functionality to improve fluent syntax in Swift.

As I said, I've been using the Then library for years, but the reality is that on every project I was a bit frustrated by it. It lacks some functionality that I miss on occasions, especially when having interactions with Kotlin code. That meant that I ended up making my own extensions on every project, and eventually switching some names to better match my preferences.

So after a long time I decided to write my thoughts on it, put together a list of requirements, clean up the methods, unit test them and release it to the world.

I present you Flow, 🌊 Let your code flow. A Swift package that includes a bunch of extensions to make fluent syntax better.

What you see is my take on this functionality. It covers my needs for fluent syntax. Of course, other libraries and languages have inspired it. I will document the direct influences on each method so you can explore the alternative yourself ;)

Below, I summarize all the functionality that I usually want:

  • The star of the show is .then. We want this to configure reference and value types. Useful for configuration at the point of initialization.
  • .mutate in place value types.
  • .let to transform an object into another.
  • .do to perform multiple actions with the same object.
  • Free function variants, for when you prefer this syntax or don't want to conform to the protocol:
    • with (similar to .then)
    • withLet (similar to .let)
    • run as an alternative to immediately executed closures.

.then

.then let's you perform an object configuration inline. It applies statements in the closure to the object. It's very useful to set the properties of an object when defining it. Is what started this entire discussion many years ago :)

let label = UILabel().then {
  $0.text = "Hello"
  $0.textColor = .red
  $0.font = .preferredFont(forTextStyle: .largeTitle)
  $0.sizeToFit()
}

let size = CGSize().then {
	$0.width = 20
}

There are two overloads of this method provided. One that works on AnyObject (a.k.a. classes) and another that operates on Any (intended for value types). The compiler picks the correct one appropriately.

  • In the closure you get a reference to self or an inout copy in case of value types.
  • It returns the same reference to the object, or the mutated copy for value types.

Influences:

.mutate

Mutates a value in place. It s like .then but applies to self instead of a new copy. The value needs to be defined as a var.

view.frame.mutate {
  $0.origin.y = 200
  $0.size.width = 300
}
  • In the closure you get an inout reference to self .
  • It returns nothing.

This should be used only for value types. For reference types is recommended to use .then.

.let

You can think of .let as a map operation but for all the types (not only for Functors). It lets you transform the object into an object of another type.

let dateString: String = Date().let {
    let formatter = DateFormatter()
    return formatter.string(from: $0)
}

It works especially well for type conversions based on initializers:

let number: Int? = "42".let(Int.init)

Don't overuse this when you can use just plain dot syntax. You can use it to access a member of the object Date().let { $0.timeIntervalSince1970 } but that's just the same as Date().timeIntervalSince1970.

  • You get a reference to self in the closure.
  • It returns the object returned in the closure.

Influences:

.do

Use this method to perform multiple actions (side effects) with the same object. It helps to reduce the verbosity of typing the same name multiple times.

UserDefaults.standard.do {
    $0.set(42, forKey: "number")
    $0.set("hello", forKey: "string")
    $0.set(true, forKey: "bool")
}

This behaves like other methods if you discard their return, but is preferred to use do to convey the intention better. It also lets you avoid writing the return on some occasions.

  • You get a reference to self in the closure.
  • It returns nothing.

Influences:

.debug

By default, it prints self to the console. This method is useful for debugging intermediate values of a chain of method calls.

let result = Object()
   .then { ... }
   .debug("prefix")
   .let { ... }
   .debug()
  • You get a reference to self in the closure.
  • It returns the same object without touching it.

The following free function variants are mostly there to workaround Swift limitations. But there are also some people that prefer them. Flow gives you both alternatives ;)

Free function with

Executes a closure with the object. This free function it's a substitute for .then when you can't use the method or if you prefer the free function style.

let label = with(UILabel()) {
    $0.text = "Hello"
    $0.textColor = .red
    $0.font = .preferredFont(forTextStyle: .largeTitle)
    $0.sizeToFit()
}
  • You get a reference to an inout copy of self in the closure.
  • It returns the returned object in the closure.

Influences:

Free function withLet

Variant of with that lets you return a different type. It's a free function alternative to let.

Free function run

Executes a closure of statements, useful to be used when you need an expression. This is like making a closure and invoking immediately, but sometimes is clearer to have a specific name for it.

let result = run { ... } // same as { ... }()

Conclusion

It has been fun to spend the last few days writing down my thoughts on this topic. I read many times Kotlin's documentation and looked at how other languages solved the problems. Ultimately, I compiled a list of the functionality that I wanted and which names should I use for them. I've been using this code in one way or another for many years, so I'm happy to finally have put the time to polish it.

One of the things I tried was to unify some methods. It worked for then which operates on both reference and value types. I wanted the mutate method to have the same name, but that didn't work out. let is one that I use quite a lot, although I try to not overuse it as the Kotlin community does, in my opinion. do is one that I don't use a lot, but is useful sometimes and since the purpose of this package is to avoid friction, I ultimately decided to include. For the same reason the free functions are included, sometimes I don't want to conform an object to the protocol just for a one of operation, and the free functions are perfect for those occasions. And don't forget about debug, probably the one I use the most ^^.

Thank you all for reading and remember you can find Flow on GitHub and try it by yourself.

If you liked this article please consider supporting me