Magical Future Swift Is (Almost) Here

I’ve been holed up dealing with Metal for a bit (which I was completely unfamiliar with, and therefore unable to blog about), but Swift 1.2 came out last week, rousing me out of my graphics-induced coma. And boy was I in for a surprise. Remember my posts on [monads], and all my talk of Magical Future Swift? It’s here!

Well…almost.

• • • • •

A brief recap for those who don’t remember: I motivated my discussion of Magical Future Swift by talking about the nested optionals problem, wherein Birthday Beth wants to have a super fun birthday party, but only if all of her friends show up:

func partyGameForFriend1(friend1:Friend?,
                        friend2:Friend?, 
                        friend3:Friend?) -> Game {
    if let f1 = friend1 {
        if let f2 = friend2 {
            if let f3 = friend3 {
                return Game.Superfun(f1, f2, f3)
            }
        }
    }

    return Game.Regular
}

Recognizing how awkward this was (as has everyone who has used Swift 1.1 and below), I made the suggestion that Magical Future Swift would allow something different: for-comprehensions:

// Magical Future Swift
func partyGameForFriend(friend1:Friend?,
                        friend2:Friend?, 
                        friend3:Friend?) -> Game {

    let optionalGame = for {
        f1 <- friend1
        f2 <- friend2
        f3 <- friend3
    } yield {
        Game.Superfun(f1, f2, f3)
    }

    return optionalGame ?? Game.Regular
}

Basically, the <- operator unwraps the optionals, and calls the yield block with the unwrapped result, but only if all the optionals are unwrapped. If you’ve looked at Swift 1.2, this should be awfully familiar. It’s the new, supercharged if-let:

// Swift 1.2
func partyGameForFriend(friend1:Friend?,
                        friend2:Friend?, 
                        friend3:Friend?) -> Game {

    if let f1 = friend1,
       let f2 = friend2,
       let f3 = friend3  {

        return Game.Superfun(f1, f2, f3)
    }

    return Game.Regular
}

I didn’t discuss the possibility at the time (and to be truthful, was only vaguely aware of it), but Swift 1.2 adds some extra sweetness to the deal in the form of guard clauses. Suppose the super fun party only works if Beth’s first two friends are still on good terms. With the new if-let syntax, we can do that without an extra if block:

// Swift 1.2
func partyGameForFriend(friend1:Friend?,
                        friend2:Friend?, 
                        friend3:Friend?) -> Game {

    if let f1 = friend1,
       let f2 = friend2,
       let f3 = friend3 
       where f1.isGoodFriendsWith(f2) {

        return Game.Superfun(f1, f2, f3)
    }

    return Game.Regular
}

This will only call the super fun game branch if all the values exist and f1 is still good friends with f2. Pretty powerful stuff. To make things even better, we can use an optional unwrapped in an early let declaration in a later let declaration. Suppose, for instance, that inviting friend3 always results in her best friend showing up (if she has one). We could write:

// Swift 1.2
func partyGameForFriend(friend1:Friend?,
                        friend2:Friend?, 
                        friend3:Friend?) -> Game {

    if let f1 = friend1,
       let f2 = friend2,
       let f3 = friend3,
       let f4 = f3.bestFriend() // Friend.bestFriend() -> Friend?
       where f1.isGoodFriendsWith(f2) {

        return Game.Superfun(f1, f2, f3)
    }

    return Game.Regular
}

This works as long as the right side of the binding (i.e. f3.bestFriend()) returns an optional—which makes sense; if it didn’t, we could simply work it in the body of the if-let block.

The only real difference between Magical Future Swift’s for-comprehensions and the if-let version is that if-let does not return a value; instead it behaves as a traditional control flow statement. But this is, in fact, a little limiting.

• • • • •

If you remember, for-comprehensions weren’t limited to optionals. They could handle any monadic type: optionals, lists, futures, errors, etc. Swift 1.2’s if-let syntax doesn’t let us do that—and the fact that if-let statements don’t return a value actually makes it somewhat difficult to adapt them.

Let’s be specific. What would it look like if we used if-let in the context of lists? Let’s review the example where Scheduling Sam tries to create match-ups between players from different cities for a chess tournament, presented as a for-comprehension:

func matchupsForCities(cities:[City]) -> [Matchup] {

    return for {
        city <- cities
        p1 <- teamAForCity(city)
        p2 <- teamBForCity(city)
    } yield {
        Matchup(city:city, player1:p1, player2:p2)
    }
}

With the appropriate semantics, the if-let idiom can be adapted:

func matchupsForCities(cities:[City]) -> [Matchup] {

    var matchups = [Matchup]()
    if let city = cities
       let p1 = teamAForCity(city)
       let p2 = teamBForCity(city) {
        matchups.append(Matchup(city:city,
                             player1:p1,
                             player2:p2))
    }

    return matchups
}

This follows the same rules as the optional binding: the right side of the let bindings must be lists, and the left side will be whatever the elements of the list are: City or Player, in our case. But there’s that nasty var that we have to declare at the beginning, because if-let doesn’t return values. And if it did return values, we’d have to introduce a new keyword, like yield, to differentiate what is supposed to be returned as a value from the if-let statement from an early return call that exits the function.

To add a further blemish, if-let isn’t very intuitive in this context. While we could get used to reading it, in the case of lists, as “do this if there are still city-and-two-player-combinations that haven’t been processed”, it’s not the first interpretation; for-let would probably work better.

So while I really like the if-let idiom for optionals, it’s not easily extensible. What happens when we deal with futures and promises? At that point, should we stick with if-let, try for-let, or have a more dedicated when-let or after-let? What about other monadic types? Does Swift have to provide specific syntax for every monad we could possibly want to use? The advantage of for-comprehensions is that they’re just generic enough that they can acceptably work in any context. The if-let idiom is less intuitive to expand.

Of course, some of this is habit, and, much like before, I have no particular intuition as to where the language is headed. In fact, I’d say that my focus on for-comprehensions made me miss the obvious if-let extensions that made optional handling better, despite them having been discussed on the developer forums a few times.

• • • • •

At the end of the day, I have to try to remember Chris Lattner’s admonition that Swift is intended above all to be a practical language. The new if-let syntax solves an acute existing problem; full for-comprehensions would be nice to have in my view, but people aren’t exactly clamoring for them (I checked by searching the dev forums; nowhere to be found).

So Magical Future Swift is almost here. But it might stay almost here for quite a while. As for myself, I look forward to Swift Next. Extrapolating the release schedule so far, it would be shipping sometime in June.

Say, does Apple have some sort of event in June where it might showcase it?

[monads]: http://nomothetis.svbtle.com/the-culmination-part-ii

 
102
Kudos
 
102
Kudos

Now read this

Error Handling in Swift: Might and Magic

If distance brings perspective, proximity brings understanding. Concepts that were remote and downright strange when I played with them in Haskell or read about them in Scala are blindingly obvious solutions to any number of problems now... Continue →