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!
- Adding Method Cascades by Erica Sadun, one of the early proposals.
- Fluent syntax (replacing void with a useful default return value) - Discussion - Swift Forums
- Circling back to
with
- Pitches - Swift Forums - Customized Inline Init Closure - Discussion - Swift Forums
- Pitch: Scoped Functions - Pitches - Swift Forums
- Discussion Here we go again: Extension Functions - Discussion - Swift Forums
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.
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!
flow-offers">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 onAny
(intended for value types). The compiler picks the correct one appropriately.
- In the closure you get a reference to
self
or aninout
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 toself
. - 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 asDate().timeIntervalSince1970
.
- You get a reference to
self
in the closure. - It returns the object returned in the closure.
Influences:
- Swift’s own
let
declaration. - Kotlin.let and Kotlin.run.
.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 ofself
in the closure. - It returns the returned object in the closure.
Influences:
- Overture.with
- Kotlin.with
- Many other languages have a
with
orusing
function.
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.