Skip to content

Instantly share code, notes, and snippets.

@pyrtsa
Last active March 7, 2017 18:39
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pyrtsa/77978129090f6114e9fb to your computer and use it in GitHub Desktop.
Save pyrtsa/77978129090f6114e9fb to your computer and use it in GitHub Desktop.
Managing optionals in Swift
// Hello, fellow Swift programmer!
//
// Here's a demonstration of a few ways of cleaning up the unwrapping of
// optionals in Swift.
//
// Swift is known to have both OO and functional background. Thus, there is
// probably some middle ground programming style that is *more* functional than
// Objective-C, and probably less so than Haskell. This playground tries to
// compare their differences in the pretty common scenario of dealing with
// errors or missing input.
import Foundation
// Let's start with a simple data structure for a logged in user (something we
// might expect an API endpoint to return to us in JSON format).
struct User {
let realname: String
let username: String
let isActive: Bool
}
// An instance of User can be created with the compiler-generated initialiser:
let user1 = User(realname: "Timmy", username: "tcook", isActive: true)
// -----------------------------------------------------------------------------
// MARK: Input data and its (fake) parsing
//
// Now, suppose we get some input which may or may not contain our name fields
// and activity status. I don't care how it's parsed. Instead, let's
// just say it's of a type `Input`, and that there are functions for turning
// an `Input` into optional names (that is, `nil` if invalid).
struct Input {}
let input = Input() // dummy value
// Tip: For playing on the playground, try changing one or more of the below
// functions into returning `nil`.
func realname(_: Input) -> String? { return "Craig" }
func username(_: Input) -> String? { return "hairforceone" }
func isActive(_: Input) -> Bool? { return true }
// -----------------------------------------------------------------------------
// MARK: Plain old `if` statement with forced unwrapping of optionals
//
// This is the most naïve way of checking against errors.
var user2: User?
if realname(input) != nil &&
username(input) != nil &&
isActive(input) != nil
{
user2 = User(realname: realname(input)!,
username: username(input)!,
isActive: isActive(input)!)
}
// -----------------------------------------------------------------------------
// MARK: Nested `if let` blocks
//
// There is no standard way of unwrapping multiple optionals, so one might do
// so with nested statements. Pretty naïve as well; does not scale well.
var user3: User?
if let r = realname(input) {
if let u = username(input) {
if let a = isActive(input) {
user3 = User(realname: r, username: u, isActive: a)
}
}
}
// -----------------------------------------------------------------------------
// MARK: Switch statement
//
// A switch statement can be used to pattern match multiple optionals at once.
var user4: User?
switch (realname(input), username(input), isActive(input)) {
case let (.Some(r), .Some(u), .Some(a)):
user4 = User(realname: r, username: u, isActive: a)
default: break
}
// Side note: If (and when) a switch statement could be used as an expression,
// we wouldn't need to use `var` for the variable. A `let` binding would be
// enough, something like `let user4 = switch (...) { case ...: User(...) }`.
// -----------------------------------------------------------------------------
// MARK: The `every` trick, or `if let (...) = every(...) { ... }`
//
// We can move our switch statement into a function to make it more versatile.
func every<A, B, C>(a: A?, b: B?, c: C?) -> (A, B, C)? {
switch (a, b, c) {
case let (.Some(a), .Some(b), .Some(c)): return .Some((a, b, c))
default: return .None
}
}
// This is something generic enough that it could be implemented in a library,
// of course with overloads for different arities (AB, ABCD, ABCDE, et cetera).
// The language support is limited here, so those overloads would need to be
// written manually. But remember: this is just library code, written once.
var user5: User?
if let (r, u, a) = every(realname(input), username(input), isActive(input)) {
user5 = User(realname: r, username: u, isActive: a)
}
// -----------------------------------------------------------------------------
// MARK: The `every` trick together with `Optional.map`
//
// Like arrays, optionals have a member function `map` to transform the wrapped
// instance with a function when it's there. We can use that to convert the
// previous `if let` statement into an expression. (That way, our `var` turns
// into a `let`-bound constant, which makes code easier to follow because we
// know that its contents won't change further below.)
let user6 = every(realname(input), username(input), isActive(input)).map {
(r, u, a) in User(realname: r, username: u, isActive: a)
}
// Note that the type of `user6` is still `User?`; the input could of course
// have been invalid.
// -----------------------------------------------------------------------------
// MARK: Currying
//
// Now this needs a little explanation. Currying is the technique of turning a
// multiple argument (i.e. tuple-argument) function into a nested function whose
// result takes more arguments one by one until there are none left. For
// example,
func and(a: Bool)(b: Bool) -> Bool { return a && b }
// is a curried version of the "AND" operator. According to the language
// specification, it should be called like:
//
// and(true)(false) // (does not compile)
//
// but due to a compiler bug, the following is currently allowed instead:
and(true)(b: false) //=> false
let trueAnd = and(true) //=> function of type Bool -> Bool
trueAnd(b: true) //=> true
// In Haskell, all functions are written that way by default. In Swift, not so
// much. In a bit, I'm trying to show that doing so makes actually quite a bit
// of sense, because it's easier to deal with functions of type `A -> B` than
// functions of arbitrary arity `(A, B, ...) -> R`.
//
// I'm presenting three ways to curry a function. The first is by creating an
// overloaded set of generic functions called `curry`. These, again, go
// naturally into a library, with no need to reimplement every time.
func curry<A, B, C, R>(f: (A, B, C) -> R) -> A -> B -> C -> R {
return {a in {b in {c in f(a, b, c)}}}
}
// We can use `curry` to convert a "maker" function into a curried one:
extension User {
static func make(realname: String, _ username: String, _ isActive: Bool) -> User {
return User(realname: realname, username: username, isActive: isActive)
}
static let create = curry(User.make)
}
User.make("Scott", "forstall", false)
User.create("Scott")("forstall")(false)
// Of course, we'd like to avoid the boilerplate of implementing the above
// function `User.make`. Unfortunately, we can't (compiler bug?) just say:
//
// let makeUser = curry(User.self) // does not compile
//
// which would be nice, but instead, we can write the following (and the
// compiler is able to figure out the types `(String, String, Bool)` from the
// call:
extension User {
static let create2 = curry{(r, u, a)
in User(realname: r, username: u, isActive: a)}
}
User.create2("Scott")("forstall")(false)
// The third way to curry a function is to just make it curried by definition.
// (I use the form `f(a: A) -> B -> R` instead of `f(a: A)(b: B) -> R` just to
// dodge the compiler bug with currying I mentioned earlier.)
extension User {
static func create3(realname: String) -> String -> Bool -> User {
return {username in {isActive in User(realname: realname,
username: username,
isActive: isActive)}}
}
}
User.create3("Scott")("forstall")(false) // Still no Corinthian leather!
// -----------------------------------------------------------------------------
// MARK: `fmap` and `apply`
// So it turns out, with just a few lines of (library) code, we can write
// functions which allow us to correctly handle `nil` values for any of the
// above curried functions. Those functions are called `fmap` and `apply` in
// Haskell. And yes, `fmap` is exactly the same as the `map` we used earlier,
// just with its arguments flipped so the function comes first.
func fmap<A, B>(f: A -> B, x: A?) -> B? {
return x.map(f)
}
// The `apply` function is similar but in its case, the function `f` is an
// `Optional` as well. Why? Because that's what `fmap` will return for a curried
// function when there are more arguments expected!
//
// (If this blows your mind, it should. The road to understanding what's
// happening here is that the type parameter `B` may as well be of function type
// `X -> Y` or `X -> Y -> Z` or even `X -> Y -> Z -> W -> R`.)
func apply<A, B>(f: (A -> B)?, x: A?) -> B? {
switch (f, x) {
case let (.Some(f), .Some(x)): return .Some(f(x))
default: return .None
}
}
// With `fmap` and `apply` in a library and one of the 3 `create` functions we
// wrote, we can get back to our example.
let part1 = fmap(User.create, realname(input)) // (String -> Bool -> User)?
let part2 = apply(part1, username(input)) // (Bool -> User)?
let user7 = apply(part2, isActive(input)) // User?
// It doesn't read pretty at all but does what we want. To make it read well,
// we'll resort to the one last trick which is really commonplace in the world
// of Haskell. (Whether it is, or will be commonplace in Swift, is what's left
// under debate here.)
// -----------------------------------------------------------------------------
// MARK: Two operators for applicative functors: `<^>` and `<*>`
//
// So yes, user-defined operators. But bear with me, these two operators are
// really well motivated. Even their symbols kinda make sense.
//
// When a function `f` is mapped over an `Optional` (or over an `Array`
// likewise), it's sometimes said to be "lifted" to the space of optionals. So a
// reasonable symbol for this operation could be an upward-pointing "arrow". The
// precedence of this operator is so it's the same as for Swift comparison
// operators. (Not going into details why this is a good idea.)
infix operator <^> { associativity left precedence 130 } // the "map" operator
// The `apply` function can be regarded as a kind of product operation, and thus
// it's widely used operator symbol contains the multiplication symbol. It has
// the same precedence as "map".
infix operator <*> { associativity left precedence 130 } // the "apply" operator
// The operator definitions are exactly the same as their function counterparts.
func <^> <A, B>(f: A -> B, x: A?) -> B? {
return x.map(f)
}
func <*> <A, B>(f: (A -> B)?, x: A?) -> B? {
switch (f, x) {
case let (.Some(f), .Some(x)): return .Some(f(x))
default: return .None
}
}
// And hey, all the magic is there already. We can just write the last example:
let user8: User? = User.create <^> realname(input)
<*> username(input)
<*> isActive(input) // TA-DA!
// Reads kinda nice, doesn't it? And if more fields are needed, we just have to
// modify our `User.create` function to take more arguments. That, in turn,
// would break the above example until we add one more `<*> foobar(input)` to
// make it return a `User?` again.
//
// Notice that replacing any (combination) of the inputs with `nil` makes
// the whole expression return `nil`:
let user9: User? = User.create <^> {_ in nil}(input)
<*> username(input)
<*> isActive(input) // returns nil
// So the logic that started with `if foo != nil` and nested `if let` statements
// still works.
//
// What comes to the parsing of the input, in Real World™, of course we wouldn't
// have those `realname(_:)` functions but instead, those would be whatever
// expressions that have the correct `Optional<T>` type. For an example, see the
// recently published Argo library https://github.com/thoughtbot/Argo which uses
// a syntax like `dict <| "key"` that even manages to omit mentioning the type
// while still keeping it type safe.
// Comments welcome.
// -- @pyrtsa
@pyrtsa
Copy link
Author

pyrtsa commented Oct 5, 2014

There's an optimisation I didn't mention in the above. Defining <*> as

func <*> <A, B>(f: (A -> B)?, x: @autoclosure () -> A?) -> B? {
    if let f = f { if let x = x() { return .Some(f(x)) }}
    return .None
}

instead allows us to delay the possibly expensive parsing of further inputs in case of an error. For example,

User.create <^> {_ in "Name"}(input)
            <*> {_ in nil}(input)
            <*> expensiveComputationOfIsActive(input)

will never get to evaluate the last argument because the second one already returned nil.

It's also possible to add this feature to the if let (...) = every(...) trick, although it adds a bit of boilerplate to the library code:

func every<A, B, C>(a: A?,
                    b: @autoclosure () -> B?,
                    c: @autoclosure () -> C?)
                    -> (A, B, C)?
{
    if let a = a { // (Intentionally avoiding the nesting indentations here.)
    if let b = b() {
    if let c = c() {
        return (a, b, c)
    }}}
    return nil
}

Example:

let dict = [1: "one"]
if let (a, b, c) = every(dict[1],
                         dict[2],
                         (assert(false), dict[3]).1)) // A crash waiting to happen.
{
    println("Got a, b and c!")
} else {
    println("Got nothing without crashing!") // Prints this without crashing.
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment