The trailing closure syntax gives us today a powerful way to make syntax extensions.
A common example is adding an extension for Int mirroring Ruby, like this:
10.times {
println("Hello")
}
This prints “Hello” 10 times.
As most of you know, this is syntax sugar for
10.times({ println("Hello"})
But it also happens to look like a real syntax construct like for, if, while etc.
Here’s the extension we use to make that happen:
extension Int {
func times(block: () -> Void) {
for _ in 0 ..< self { block() }
}
}
However, if you tried to do something like this:
10.times {
println("Hello")
return
}
You also see “Hello” printed 10 times. Our illusion – that this is a normal block like for if/while/etc – breaks.
For normal closures there is no way to fix this problem. We can only have the closure return to it’s caller, which in this case is the function times we added to Int.
In many languages with macros, we would construct syntax by directly working with the syntax tree created by the compiler. This is can get fairly advanced and isn’t very easy to read.
As we see in this example we could do a lot of nice things if we just had something which “looks like a closure but unwinds the stack like an exception”
Here is how it could look:
extension Int {
func times(block: @localscope () -> Void) {
for _ in 0..self {
block()
}
}
}
The @localscope annotation would change the semantics of the “closure” in the following way:
- return would mean return in the outer scope
- continue would mean return from the closure
- break would mean return from the function
The special behaviour would be restricted to closures defined inline, not closures created elsewhere. So this:
10.times {
println("Hello")
return
}
prints “Hello” once, but
let helloFunc = { println("Hello"); return }
10.times(helloFunc)
would print “Hello” 10 times (just like the normal closure version)
A few examples:
// Implementing "unless"
func unless(comparison: @autoclosure ()->Bool, block: @localscope ()->Void) {
if !comparison() { block() }
}
func max(a: Int, b: Int) -> Int {
unless(a > b) { return b }
return a
}
// Implementing let-else
func letElse<T>(object : T?, _ elseBlock: @localscope ()->T {
if let object = object { return object }
return elseBlock()
}
func printOrExit(string: String?) -> Bool {
let string = letElse(string) { return false }
println(string)
return true
}
printOrExit(nil) // => returns false, prints nothing
printOrExit("foo") // => return true, prints "foo"
Many more things can be created with this fairly simple syntax extension – this is just scratching on the surface of what’s possible.
A full macro system for Swift would be awesome, but if we can’t get that – something like these proposed pseudo-closures would fix many of the use cases where closures are nearly enough but not quite there.
2 thoughts on “Macro-like syntax extensions through pseudo-closures – a syntax proposal”