mokacoding

unit and acceptance testing, automation, productivity

Writing your own Swift "if let"

In the previous post we looked under the hood of Swift's optionals, and discovered that writing String? actually means writing Optional<String>. Optional is a type in its own right, defined as an enum.

To consolidate our understanding of how optional works let's try to implement a custom version of Swift's if let ... else construct.

let optionalString: String? = ...
if let string = optionalString {
  print(string)
} else {
  print("Wooops! No string")
}

We can imagine if let ... else as a function that takes a String? as its input, together with two other functions. One will be String -> (), the then branch of the conditional which executes if the unwrapping of the input value is successful; the other () -> (), which executes otherwise.

func ifLet(_ value: String?, then: String -> (), else elseFunc: () -> ()) {
  // ...
}

How do we find out if value is null or not, without using if let? We could check for nullability like if (value == null), or we could remember that Optional is an enum type, and use pattern matching, which is way neater:

func ifLet(_ value: String?, then: String -> (), else elseFunc: () -> ()) {
    switch value {
    case .some(let string): then(string)
    case .none: elseFunc()
    }
}

We can use our custom if let like this:

ifLet(optionalString,
    then: { print($0) },
    else: { print("Wooops! No string") }
)

Our ifLet(value: then: else:) is restricted to String? as its input, but what if we wanted to use it with other types of optionals? Int?, Double?, NSDate?, etc.

Let's make it generic!

func ifLet<T>(value: T?, then: T -> (), else elseFunc: () -> ()) {
    switch value {
    case .some(let x): then(x)
    case .none: elseFunc()
    }
}

Nice ☺️. Now we can pass any kind of inputs of our ifLet. But we're still a bit constrained, for example we couldn't replicate this behaviour:

let input: String? = "a string"
let output: Int = {
    if let string = input  {
    return Array(string.characters).count
    } else {
    return -1
    }
}()

So let's add a return value in the mix, generic of course:

func ifLet<T, U>(
    _ value: Optional<T>,
    then thenFunction: (T) -> U,
    else elseFunction: () -> U) -> U {

    switch value {
    case .some(let x): return thenFunction(x)
    case .none: return elseFunction()
    }
}

let o = ifLet(
    input,
    then: { x in
        return Array(x.characters).count
    },
    else: {
        return -1
    }
)

That's it. Now we have a full fledged custom if let ... else implementation... which we obviously never use because the original one is way easier to work with.

Nevertheless I think that going through this kind of exercises is a good way to consolidate new concepts, and stretch oneself as a developer.

All the code in this post is available in this Playground on GitHub.

Feel free leave a comment below or to get in touch with me on Twitter @mokagio if you found something wrong in the code, you'd like to suggest more of this kind of exercises, or you just wanna chat.

Leave the codebase better than you found it.

Want more of these posts?

Subscribe to receive new posts in your inbox.