DEV Community

Cover image for Learn Swift From the Standard Library: A Deep Dive Example
rlemasquerier
rlemasquerier

Posted on

Learn Swift From the Standard Library: A Deep Dive Example

Swift is a very powerful language. It has a lot of nice built-in capabilities to improve both performances and efficiency that one get to discover along the way while becoming more familiar with the language.

And a few days ago, I found out that one of the best way to discover new features is to explore an amazing showcase of Swift language characteristics, which is... The Swift Standard Library itself!

To convince you, I'll show you in this article how the && operator from the Bool class is implemented, and how it helps us understand really well two very nice concepts, namely:

  • The rethrows declaration (I wish I had discovered this before)
  • The @autoclosure attribute

I learnt about this example in this awesome talk from Paul Hudson, which I recommend you to watch.

I thought I could explain this from another angle, by trying to reimplement the operator from scratch by ourselves - so that we discover on the road how we come to actually need those interesting concepts.

So, let's reimplement the && operator of the Swift Core library!

If you come from the JavaScript world (or some other languages), be careful, && works only with actual Bool types (true && "Hello" won't compile). Keep that in mind if you plan to give it a try by yourself.

How would you implement such an operator ? Try to think about it, it will help you understand later why we would need such things like rethrow.

To try it at home, open a playground in Xcode (File > New > Playground). In 30 seconds max, you have a sandbox ready to make your experiments.

To make tests without conflicting with the existing operator, let's define a new operator, and name it for instance &&&:

precedencegroup CustomAnd {
}
infix operator &&&: CustomAnd

extension Bool {
    public static func &&& () -> {}
}

Now let's try to implement it. This is my first shot:

public static func &&& (lhs: Bool, rhs: Bool) -> Bool {
   return lhs ? rhs : false
}

This looks good, right ? We take two parameters, and we return true only if the left hand side lhs is true as well a the right hand side rhs.

Let's see if this works along with a small function:

func canVote(withAge age: Int) -> Bool {
    print("verifying that user is allowed to vote...")
    return age >= 18
}

print(true &&& canVote(withAge: 30))

It works, right ? We see following logs, and that's what we expected:

> verifying that user is allowed to vote...
> true

Let's try this example now:

print(false &&& canVote(withAge: 30))
> verifying that user is allowed to vote...
> false

Oh, Crap! This is not what we expected. I mean OK, the final result is correct, but I didn't expect the first line here. In Swift, the && operator is lazy, we don't want the right hand side to be evaluated if the left one is false!

To handle this, what about considering the right hand side as a closure, that would be executed only when we need to ? Sounds like a good idea, let's do this:

public static func &&& (lhs: Bool, rhs:  () -> Bool) -> Bool {
   return lhs ? rhs() : false
}

rhs is now a function, so we need to pass it a closure. Have a look at the doc about closures:

Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.

So now, we could use our operator like this:

print(false &&& { canVote(withAge: 30) })

Wow bro, it solves the problem, but that's not cool at all.

Yeah, sorry. We want the right hand side to keep looking as a an actual boolean, it's ugly having to explicitely pass a closure.

Swift saves us here with the @autoclosure attribute for our parameter to handle that:

public static func &&& (lhs: Bool, rhs: @autoclosure () -> Bool) -> Bool {
    return lhs ? rhs() : false
}

The function signature is the same, it expects a function as a right hand side parameter. But this time, you can do it implicitely: Swift will create the closure returning the expression for us.

So cool, it really solves the issue ! Now, if the left side is false, the right side won't be evaluated, and I can still use my operator easily without knowing that I'm using a closure here.

I have a warning though: ⚠️ Don't overuse autoclosures in the everyday life. It's a syntaxic conveniance to omit braces when it makes sense, you should probably keep them in most cases to make it clear that it may not be executed.

This kind Note in Swift doc state it well:

Overusing autoclosures can make your code hard to understand. The context and function name should make it clear that evaluation is being deferred.

So we found a solution, but did you notice the new problem it raises? What if we use an expression that can throw ? Before using an autoclosure, it was not a problem since the left-hand side and the right-hand side where evaluated before being passed to our function, so we could use a try statement as usual.

For instance, let's consider this function which can throw if we feed it with an integer lower than 0:

func canVote(withAge age: Int) throws -> Bool {
    guard age > 0 else {
        throw CustomError.NegativeAgeError
    }
    return age >= 18
}

Before using @autoclosure, following code could compile:

print(try true &&& canVote(withAge: 30))

But now, Swift compiler gets angry because we specified that the second parameter is a closure that do not throws, as clearly explained by the error:

> Call can throw, but it is executed in a non-throwing autoclosure

Well, I have to admit that the compiler is definitely right.

If we want to be able to use our custom &&& parameter with an expression that can throw (and we will!), we need to add the declaration to the rhs autoclosure and use a try before calling it. So let's rewrite it:

extension Bool {
    public static func &&& (lhs: Bool, rhs: @autoclosure () throws -> Bool) -> Bool {
    return lhs ? try rhs() : false
    }
}

Arf. It's still not compiling:

> Errors thrown from here are not handled

Well, that makes sense. Our operator should either handle errors or throw now. Let's go with the second solution, and add the declaration:

extension Bool {
    public static func &&& (lhs: Bool, rhs: @autoclosure () throws -> Bool) throws -> Bool {
    return lhs ? try rhs() : false
    }
}

Yay, it works. Now I can use a throwing expression on the right hand side as well.

But do yo see the new problem here ?

Yes that's right ! Now, our operator is a throwing function, so I have to use a try statement. Now we can't even compile a simple false && true! (We would get Operator can throw but expression is not marked with 'try')

And that's where, if you are not already, should become a Swift enthusiast.

We can use the rethrows declaration: It means that our method throws only if it's used with a closure which throws. All that simple.

If we come back to our problem, we now write:

extension Bool {
    public static func &&& (lhs: Bool, rhs: @autoclosure () throws -> Bool) rethrows -> Bool {
    return lhs ? try rhs() : false
    }
}

And now we can write true && false without any issues, because the closure generated for the right hand side doesn't throw, so thanks to rethrows, our operator is not a throwing function, and thus, no need to use try.

On another hand, if we use a throwing expression, then we need to add a try. From the developer point of view, the behaviour is exactly the same as if we were passing two boolean, as our first naive implementation.

This is the actual implementation of the && operator in the Swift Standard Library. You can double check it in swift source code.

I'm really amazed by the number of concepts such a basic feature contains:

If you're interested to go deeper, this video from Paul Hudson showcase the swift standard library with this particular example, as well as others even more interesting, focused on improving security and performance.

The entire Swift core library is full of interesting patterns and optimisations. It's definitely worth to have look inside it, if you want to randomly discover new interesting concepts, or find an example of something you already know, but don't understand well.

Latest comments (0)