On the last two projects I worked on, I have used this extension on Optional
:
import Foundation
extension Optional {
func unwrap(orThrow error: @autoclosure () -> Error) throws -> Wrapped {
guard let value = self else {
throw error()
}
return value
}
func unwrap(or fallback: Wrapped) -> Wrapped {
guard let value = self else {
return fallback
}
return value
}
}
The unwrap(or:)
function is equivalent to, and more verbose than, the ??
operator. However, I find it works well in a chain of function calls. For example:
func fetchRecord(id: String) async throws -> Record {
Record(fragment:
try await client.fetch(query: RecordQuery(id: id))
.record
.unwrap(orThrow: ServiceError.notFound)
.fragments
.recordFragment
)
}
func fetchRecords() async throws -> [Record] {
try await client.fetch(query: RecordsQuery())
.records
.unwrap(or: [])
.map {
$0.fragments.recordFragment
}
.map {
Record(fragment: $0)
}
}
This lets me succinctly express a series of operations and transformations in a style that is similar to what you would write using Combine.
Usually, I would stop there. But I was wondering if I could merge the two functions. Something like this:
extension Optional {
func unwrap(or fallback: @autoclosure () throws -> Wrapped) throws -> Wrapped {
guard let value = self else {
return try fallback()
}
return value
}
}
That allows us to write this, just as before:
let value: String? = nil
print(value.unwrap(or: "nothing")
I wanted to be able also to write this, but it doesn't compile:
try print(value.unwrap(or: throw ServiceError.notFound))
error: expected expression in list of expressions
try print(value.unwrap(or: throw SomeError()))
^
Why doesn't that work?
I'm trying to pass throw SomeError()
as an autoclosure. This is what the Swift Programming Language book has to say about autoclosures:
An autoclosure is a closure that’s automatically created to wrap an expression that’s being passed as an argument to a function. It doesn’t take any arguments, and when it’s called, it returns the value of the expression that’s wrapped inside of it. This syntactic convenience lets you omit braces around a function’s parameter by writing a normal expression instead of an explicit closure.
So why can't we pass throw SomeError()
to as an autoclosure? It all comes down to the difference between a statement and an expression.
As a refresher:
An expression lets you access, modify, and assign values. In Swift, there are four kinds of expressions: prefix expressions, infix expressions, primary expressions, and postfix expressions. Evaluating an expression returns a value, causes a side effect, or both.
A statement groups expressions and controls the flow of execution. In Swift, there are three kinds of statements: simple statements, compiler control statements, and control flow statements.
Basically, an expression evaluates to a value and a statement does not. throw SomeError()
is a statement — specifically, a control flow statement. It does not evaluate to a value, so we can't pass it as an autoclosure.
Maybe we could do this with an overload. Let's try this:
extension Optional {
func unwrap(or error: @autoclosure () -> Error) throws -> Wrapped {
guard let value = self else {
throw error()
}
return value
}
func unwrap(or fallback: Wrapped) -> Wrapped {
guard let value = self else {
return fallback
}
return value
}
}
So now we have the same function name, unwrap(or:)
, that we can call in either of these ways:
let value: String? = nil
do {
print(value.unwrap(or: "nothing")
try print(value.unwrap(or: ServiceError.notFound))
}
catch {
print("Error: \(error)")
}
This works as expected. It prints:
nothing
Error: notFound
Things get weird, though, when we deal with optional errors:
let error: Error? = nil
print(error.unwrap(or: ServiceError.notFound)
This fails to compile with this error:
error: ambiguous use of 'unwrap(or:)'
print(error.unwrap(or: Fail()))
^
note: found this candidate
func unwrap(or error: @autoclosure () -> Error) throws -> Wrapped {
^
note: found this candidate
func unwrap(or fallback: Wrapped) -> Wrapped {
^
This isn't the end of the world — we generally don't deal with optional errors. But I don't like leaving this sort of rough edge on an API. So I think I'll stick with my original approach: Optional.unwrap(orThrow:)
and Optional.unwrap(or:)
.