Sun, 8 Jun 2014 #
Creating a Swift syntax extension: the Lisp 'cond' function
It has been less than a week since Apple announced Swift, and people are already writing libraries, snippet collections, or best practice posts. I suppose most did not even find the time to thouroughly read the Swift Book yet 1.
Some Examples are the Swift Cheatsheet, Swiftz, a functional programming paradigm library for Swift, or blog posts like this one.
1 Simple Custom Syntax
One Swift cricitism has been the lack of a macro system that would allow users to freely extend the language like Lisp (and other homoiconic) languages do. While this is indeed not possible, there is a hidden gem in the Swift language that gives us some flexibility in this domain. If you look at the Swift documentation for the various operators, you'll find this line:
func &&(lhs: LogicValue, rhs: @auto_closure () -> LogicValue) -> Bool
The &&
operator has two arguments: The left hand side and the right hand side. Swift first checks the left hand side. If this evaluates to true, only then will it evaluate the right hand side. This means that the right hand side expression will defer evaluation until the left hand side was evaluated.
How does Swift do this? Through the magic @auto_closure
syntax. This tells Swift to wrap the expression into a closure and only evaluate it if explicitly told to do so. The full function might look something like this
func &&(lhs: LogicValue, rhs: @auto_closure () -> LogicValue) -> Bool { // Proof for left hand side if lhs { // Only then execute and thus proof right hand side if rhs() == true { return true } } return false }
This is an interesting feature and allows us to implement more complex constructs with a bit of tinkering. How about implementing something new, namely the cond
expression from Lisp?
2 Writing a cond expression for Swift
In Lisp, cond
works as follows:
(cond ((= a 1) "a is one") ((= a 2) "a is two") ((= a 4) "a is four"))
Much like a switch
statement, different cases are being tested for. Each line defines a test and after that a expression result. So if a equals 2 the switch statement will return "a is two". If a equals 1, it will return "a is one".
In comparison to switch
, cond
also allows to test for different variables in one statement (i.e. is a 2 or is b 3). Another difference is that cond
returns a value, while the Swift switch
statement does not (there's actually a rdar bug for this)
Given the @auto_closure
feature from above, we can actually implement cond
in Swift ourselves.
- We need an expression that evaluates to bool
() -> Bool
- We need an expression that evaluates to Any result type
() -> Any
These should be wrapped into closures, so that they will be evaluated sequentially and only if necessary. Lets start with a simple implementation and build upon that.
Imagine the following situation: You're processing user input, and you want to delete something from either the memory or the database, depending on what the user selected.
We want something that works for the current theoretical code:
var a = get_user_input() var b = get_current_entity() // delete fro memory or db depending on what user selected var result = cond(a == 1, delete_from_db(b), a == 2, delete_from_memory(b)) // make sure we deleted the correct one if result != b ....
The important part here is that delete_from_db
and delete_from_memory
are only being executed if the condition 'a == ?' is true for the selected case. Under no circumstance do we want our entity to be deleted from the db and the memory.
3 A first version
func cond_1 (a1: @auto_closure () -> Bool, b1: @auto_closure () -> Any, a2: @auto_closure () -> Bool, b2: @auto_closure () -> Any, bf: @auto_closure () -> Any) -> Any { if a1() { return b1() } else if a2() { return b2() } return bf() }
There's our expression. We can hand it two different cases and a default expression (as a fallback). Lets see how it works. We will define two simple functions that perform side effects (imagine deleting a file, or writing to a file).
func perform_side_effects1() -> Any { println("modify a state") return 1 } func perform_side_effects2() -> Any { println("modify another state") return 2 } var a = 1 var ctest = cond_1(a == 1, 2 + perform_side_effects1(), a == 2, 3 + perform_side_effects2(), 0) // ctest is 3 (2 + 1) // prints only "modify a state" ctest
4 Handling more cases
This works fine, but it has a limitation in that it only works for two cases. This is clearly not optimal. Thankfully, Swift is pretty good at function overloading so we can simply define more functions with more cases and let swift do the hard work of figuring out which one to choose:
func cond_2 (a1: @auto_closure () -> Bool, b1: @auto_closure () -> Any, a2: @auto_closure () -> Bool, b2: @auto_closure () -> Any, bf: @auto_closure () -> Any) -> Any { if a1() { return b1() } else if a2() { return b2() } return bf() } func cond_2 (a1: @auto_closure () -> Bool, b1: @auto_closure () -> Any, a2: @auto_closure () -> Bool, b2: @auto_closure () -> Any, a3: @auto_closure () -> Bool, b3: @auto_closure () -> Any, bf: @auto_closure () -> Any) -> Any { if a1() { return b1() } else if a2() { return b2() } else if a3() { return a3() } return bf() }
You can then extend this to ever more cases. Of course, this smells like code duplication and there is a way around this, but that currently crashes the Swift Compiler. I'll come back to how to (at least theoretically) do this at the end. First, there is another issue that we need to address.
5 Making it generic
Our current version succumbs every variable down to the Any
type which means that the compiler won't be able to perform advanced type inference for anything that comes out of this function. For example, the following will not work because even though we're clearly returning Int
our actual function is set to return Any
// Causes an error var b:Int = cond_2(0 == 1, 1, 0 == 2, 2, 3) // Works fine var b:Any = cond_2(0 == 1, 1, 0 == 2, 2, 3)
Since types give us safety we'd rather have a funcion that tells us if we accidentally try to do something wrong here. Thankfully, Swift has support for Generic Programming and only a simple change is necessary for this:
func cond_3<T> (a1: @auto_closure () -> Bool, b1: @auto_closure () -> T, a2: @auto_closure () -> Bool, b2: @auto_closure () -> T, bf: @auto_closure () -> T) -> T { if a1() { return b1() } else if a2() { return b2() } return bf() } // Works fine! var b:Int = cond_3(0 == 1, 1, 0 == 2, 2, 3)
That's it, now we're telling Swift that this function handles items of type T and that the very same type T is the return type of this funcion.
Through this, we can use our cond_3 function on types of any kind like String, Array, Int, or custom types.
6 Wrap up
That's it! We've implemented our own syntax extension for Swift! Now we can write code this as this
var a = get_user_input() var b = get_current_entity() // delete fro memory or db depending on what user selected var result = cond(a == 1, delete_from_db(b), a == 2, delete_from_memory(b)) // make sure we deelted the correct one if result != b ....
Now, the entity would only be deleted from db or from memory if a contains the correct variable. Keep in mind that this was only a simple example. Much more is possible if you use this in a smart way.
Even better if one combines this in a neat way with the first closure as body mechanism or with operators. I'm sure that we can already create some pretty stunning syntax extensions with this. However, this, only if it doesn't crash the compiler, which brings us to the next and final section.
7 Limitations
All current Swift projects suffer from the current slightly beta state of the build tools and the Swift compiler. This leads to much code that looks a bit cumbersome but can easily be improved in the future. In this case, here's some untested code, that wraps the complete logic in one big function and only defines additional lightweight wrappers to deal with the parameters variations. Still not optimal but much more readable. Sadly, this crashes the compiler (actually it kills Xcode the minute you type it in, which is pretty impressive.2).
func _cond<T> (a1: @auto_closure () -> Bool, b1: @auto_closure () -> T?, a2: @auto_closure () -> Bool, b2: @auto_closure () -> T?, a3: @auto_closure () -> Bool, b3: @auto_closure () -> T?, a4: @auto_closure () -> Bool, b4: @auto_closure () -> T?, a5: @auto_closure () -> Bool, b5: @auto_closure () -> T? ) -> T? { if a1() { return b1() } else if a2() { return b2() } else if a3() { return b3() } else if a4() { return b4() } else if a5() { return a5() } return nil } // two parameter implementation func cond<T>(a1: @auto_closure () -> Bool, b1: @auto_closure () -> T, a2: @auto_closure () -> Bool, b2: @auto_closure () -> T, bf: @auto_closure () -> T) -> T { if let r = _cond(a1, b1, a2, b2, false, {}(), false, {}(), false, {}()) { return r } return bf() } // three parameter implementation func cond<T>(a1: @auto_closure () -> Bool, b1: @auto_closure () -> T, a2: @auto_closure () -> Bool, b2: @auto_closure () -> T, a3: @auto_closure () -> Bool, b3: @auto_closure () -> T, bf: @auto_closure () -> T) -> T { if let r = _cond(a1, b1, a2, b2, a3, b3, false, {}(), false, {}()) { return r } return bf() }
I'm not sure if this would lead to correct results if it would work, but based on my current understanding of the language it should be fine.
8 Update
bjustin offered this slight modification that makes it easy to use unlimited cases. The only downside is that the fallback has to be the first item.
func cond<T>(#fallback: T, testsAndExprs: (test: @auto_closure () -> Bool, expr: @auto_closure () -> T)...) -> T { for (t, e) in testsAndExprs { if t() { return e() } } return fallback } // And in use: // y is assigned "0 == 0, of course" let y = cond(fallback: "fallback", (test: false, expr: "not this branch"), (test: 0 == 0, expr: "0 == 0, of course"))
If you read this far, you should follow me (@terhechte)
on Twitter