Improving Optionals
All problems can be solved by adding another protocol
Optionals are great. I've tracked down too many bugs to Objective-C's magical "messages to nil
return nil
"; I have no interest in going back.
But sometimes there are things you want to do with optionals or when optionals are of a specific type. Here are some of my favorites:
isNilOrEmpty
Sometimes I don't care about the difference between nil
and isEmpty == true
. First create a protocol _CollectionOrStringish
. The protocol is empty here, we're just using it to mark the types that have an isEmpty
property.
protocol _CollectionOrStringish {
var isEmpty: Bool { get }
}
extension String: _CollectionOrStringish { }
extension Array: _CollectionOrStringish { }
extension Dictionary: _CollectionOrStringish { }
extension Set: _CollectionOrStringish { }
Next we extend Optional where Wrapped: _CollectionOrStringish
:
extension Optional where Wrapped: _CollectionOrStringish {
var isNilOrEmpty: Bool {
switch self {
case let .some(value): return value.isEmpty
default: return true
}
}
}
let x: String? = ...
let y: [Int]? = ...
if x.isNilOrEmpty || y.isNilOrEmpty {
//do stuff
}
value(or:)
This one is really simple. It's the ??
nil-coalescing operator but as a function.
extension Optional {
func value(or defaultValue: Wrapped) -> Wrapped {
return self ?? defaultValue
}
}
I use this one when code starts to turn into operator-soup, anywhere the function form provides clarity, or if I need to use nil-coalescing as a function parameter:
// operator form
if x ?? 0 > 5 {
...
}
// function form
if x.value(or: 0) > 5 {
...
}
apply(_:)
This is really just a version of map
without a return value (or returning ()
if you prefer).
extension Optional {
/// Applies a function to `Wrapped` if not `nil`
func apply(_ f: (Wrapped) -> Void) {
_ = self.map(f)
}
}
flatten()
Update: Victor Pavlychko in the comments points out we can accomplish flatten()
with ExpressibleByNilLiteral
which is a great simplification!
protocol OptionalType: ExpressibleByNilLiteral { }
// Optional already has an ExpressibleByNilLiteral conformance
// so we just adopt the protocol
extension Optional: OptionalType { }
extension Optional where Wrapped: OptionalType {
func flatten() -> Wrapped {
switch self {
case let .some(value):
return value
case .none:
return nil
}
}
}
I'm leaving the original implementation for educational purposes because it demonstrates a technique you can use when ExpressibleByNilLiteral
doesn't apply.
Original flatten:
If you've ever ended up with a double-optional you might appreciate this extension. It requires some protocol and extension trickery to find a way to construct the none
case for any arbitrary Wrapped
. If that sounds obtuse then congratulations: there is still hope for you to live a normal and productive life. Here is the long boring breakdown:
- Normally compiler magic lets you assign
nil
to any sort ofOptional<Wrapped>
, even nested ones, and everything just works. - We need to make
Optional
adopt our protocol so we can give it an abstract type member (associated type) to represent the return fromflatten()
.- If we could reference self in an extension and omit the generic parameter like this:
extension Optional where Wrapped: Optional
then we could specifyflatten() -> Wrapped.Wrapped
. Unfortunately that's not the world we live in.
- If we could reference self in an extension and omit the generic parameter like this:
- The normal optional magic won't work because the protocol extension promises to return the associated type
WrappedType
. The compiler magic can't promotenil
to.none
for us.- If we could constrain
WrappedType: Optional<?>
it would work but we can't. - If we could constrain
WrappedType: Self
it would work but we can't.
- If we could constrain
- In our protocol we add an
init()
requirement. We can use that to construct and return an instance ofWrappedType
. - In the
OptionalType
extension we can useself = nil
because the compiler knowsSelf
is an optional and so it invokes the magic.
protocol OptionalType {
associatedtype WrappedType
init()
}
extension Optional: OptionalType {
public typealias WrappedType = Wrapped
public init() {
self = nil
}
}
extension Optional where Wrapped: OptionalType {
func flatten() -> WrappedType {
switch self {
case .some(let value):
return value
case .none:
return WrappedType()
}
}
}
Some of the limitations noted may eventually be lifted with various enhancements to the type system.
valueOrEmpty()
This is another small convenience when a type as an empty representation and I can't be bothered to nil-coalesce with it:
/// A type that has an empty value representation, as opposed to `nil`.
public protocol EmptyValueRepresentable {
/// Provide the empty value representation of the conforming type.
static var emptyValue: Self { get }
/// - returns: `true` if `self` is the empty value.
var isEmpty: Bool { get }
/// `nil` if `self` is the empty value, `self` otherwise.
/// An appropriate default implementation is provided automatically.
func nilIfEmpty() -> Self?
}
extension EmptyValueRepresentable {
public func nilIfEmpty() -> Self? {
return self.isEmpty ? nil : self
}
}
extension Array: EmptyValueRepresentable {
public static var emptyValue: [Element] { return [] }
}
extension Set: EmptyValueRepresentable {
public static var emptyValue: Set<Element> { return Set() }
}
extension Dictionary: EmptyValueRepresentable {
public static var emptyValue: Dictionary<Key, Value> { return [:] }
}
extension String: EmptyValueRepresentable {
public static var emptyValue: String { return "" }
}
public extension Optional where Wrapped: EmptyValueRepresentable {
/// If `self == nil` returns the empty value, otherwise returns the value.
public func valueOrEmpty() -> Wrapped {
switch self {
case .some(let value):
return value
case .none:
return Wrapped.emptyValue
}
}
/// If `self == nil` returns the empty value, otherwise returns the result of
/// mapping `transform` over the value.
public func mapOrEmpty(_ transform: (Wrapped) -> Wrapped) -> Wrapped {
switch self {
case .some(let value):
return transform(value)
case .none:
return Wrapped.emptyValue
}
}
}
descriptionOrEmpty
The new Swift 3 warning about including an optional in an interpolated string is great; most of the time you don't want "(nil)" to show up in your string. But when you either want that behavior or you just want the empty string these are handy properties:
extension Optional {
var descriptionOrEmpty: String {
return self.flatMap(String.init(describing:)) ?? ""
}
var descriptionOrNil: String {
return self.flatMap(String.init(describing:)) ?? "(nil)"
}
}
Conclusion
Hopefully this is useful or amusing. I've got a few more posts coming up with random extensions like this.
I've also got a really big post in the works but it takes a lot of work and I'm writing this for free so you'll have to be patient.
This blog represents my own personal opinion and is not endorsed by my employer.