I've been struggling with this for as long as I used Swift
To me, "chainability" is an essential part or readable, maintainable code, but the "optional chaining" sugar that Swift offers doesn't work in all use cases, unfortunately. The problem is mostly related to chains where there's an Optional
somewhere, but not in the last call.
Consider the following code:
struct Foo {
var bar: Bar
}
struct Bar {
var baz: Baz?
}
struct Baz {
var value: String
}
let foo = Foo(bar: Bar(baz: Baz(value: "howdy")))
let base: Int? = foo.bar.baz?.value.count /// we would like to default `.value` to `"yello"`
Instead of defaulting base
to "yello".count
, we would like to default Baz.value
to `"yello".
There are several options to tackle the problem, and none seems optimal to me. I usually define a method on Optional
like the one proposed by @DevAndArtist, but I call it get(or:)
, that is:
extension Optional {
func get(or defaultValue: @autoclosure () -> Wrapped) -> Wrapped {
self ?? defaultValue()
}
}
To consider all options, let's also define a |>
"left function application" operator.
precedencegroup LeftFunctionApplicationPrecedence {
associativity: left
higherThan: AssignmentPrecedence
lowerThan: TernaryPrecedence
}
infix operator |> : LeftFunctionApplicationPrecedence
func |> <A, B> (_ lhs: A, _ rhs: (A) -> B) -> B {
rhs(lhs)
}
Now, I can think of these options:
let option1 = foo.bar.baz?.value.get(or: "yello").count /// doesn't work
let option2 = (foo.bar.baz?.value ?? "yello").count /// works but requires parenthese to be chained
let option3 = foo.bar.baz.get(or: Baz(value: "yello")).value.count /// works, but it's awkward
let option4 = foo.bar.baz.map { $0.value }.get(or: "yello").count /// works, but requires explicit `map`
let option5 = Optional(foo).flatMap { $0.bar.baz?.value }.get(or: "yello").count /// works, but it's verbose
let option6 = foo.bar.baz?.value ?? "yello" |> { $0.count } /// works, but requires extra operator and precedence rules
In my code I tend to use both option2
, because it's not that bad and it's idiomatic, and option4
, in which I simply renounce the syntactic sugar of optional chaining and use the actual methods on Optional
(I'd suggest this option to those who want full chainability). option6
considers a different approach that's actually very good in terms of chainability, but requires more work up front and can be harder to understand to some (also, uses a custom operator, and personally I tend to prefer methods).