Last active
March 7, 2017 18:39
-
-
Save pyrtsa/77978129090f6114e9fb to your computer and use it in GitHub Desktop.
Managing optionals in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There's an optimisation I didn't mention in the above. Defining
<*>
asinstead allows us to delay the possibly expensive parsing of further inputs in case of an error. For example,
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:Example: