Maybe an Enum
July 23, 2018
Last time we implemented an optional as a list that contained zero or one elements of a generic type.
This time we'll re-implement optional in a more standard way using enums and add some behavior beyond the map() and filterMap() methods we implemented last time.
Today's code comes from this sample code and here's the Playground it comes from.
This time we implement an optional using an enum with two cases none and some where the some case has an associated value keeping the wrapped value of the optional.
I know - what a horrible way to describe it. I'm assuming that optionals aren't new to you and that we're just playing with a possible implementation.
enum Opt<Wrapped> {
case none
case some(Wrapped)
}
Last time we had simple inits for creating an instance that was nil or non-nil. We do that again along with computed properties for value and isNil and description.
extension Opt {
init(_ value: Wrapped) {
self = Opt.some(value)
}
init() {
self = Opt<Wrapped>.none
}
var value: Wrapped {
if case .some(let value) = self {return value}
else {fatalError("Tried to unwrap nil")}
}
var isNil: Bool {
if case .none = self {return true}
else {return false}
}
}
extension Opt: CustomStringConvertible
where Wrapped: CustomStringConvertible {
var description: String {
if isNil {return "nil"}
else {return "Opt(" + value.description + ")"}
}
}
I'm not happy with the fatal error being thrown in value.
var value: Wrapped {
if case .some(let value) = self {return value}
else {fatalError("Tried to unwrap nil")}
}
We'll revisit that later.
For now, we can take this for a test drive using the echoEvenOrNil() method from last time.
func echoEvenOrNil(_ input: Int) -> Opt<Int> {
guard input.isEven else { return Opt()}
return Opt(input)
}
echoEvenOrNil(2)
echoEvenOrNil(3)
The results are Opt(2) and nil as expected.
Let's continue with the output of echoEvenOrNil and see if the result is an optional wrapping a value that is divisible by 3.
This time we'll define filter() for Opt.
extension Opt {
func filter(_ f: (Wrapped) -> Bool) -> Opt {
switch self {
case .some(let value):
if f(value) {return self}
fallthrough
default:
return .none
}
}
}
Now we can define a closure that returns true if an int is divisible by 3.
let contentsDivisibleBy3 = {x in x % 3 == 0}
We can now filter the results of calling echoEvenOrNil and get only those numbers that are divisible by 6.
echoEvenOrNil(2).filter(contentsDivisibleBy3)
echoEvenOrNil(3).filter(contentsDivisibleBy3)
echoEvenOrNil(12).filter(contentsDivisibleBy3)
We get nil, nil, and Opt(12).
What about feeding the result of echoEvenOrNil into this function that halves ints?
func halve(_ input: Int) -> Int {
return input/2
}
The output of echoEvenOrNil is Opt<Int> and the input of halve is Int. We need map(). Here's how we implement map() for Opt. We can't use List as we did last time since this Opt doesn't build on List.
There are lots of ways to implement map(). Here's one.
extension Opt {
func map<Output>(_ f: @escaping (Wrapped) -> Output)
-> Opt<Output> {
guard case .some(let value) = self else {return .none}
return .some(f(value))
}
}
func <^><A, B>(a: Opt<A>,
f: @escaping (A) -> B) -> Opt<B> {
return a.map(f)
}
We can use it the same as we did last time.
echoEvenOrNil(2).map(halve)
echoEvenOrNil(3).map(halve)
For our flatMap example I introduced a dictionary and a simple lookup function:
let numberDictionary = [1: "one", 2: "two", 3: "three"]
func valueInNumberDictionary(for key: Int) -> Opt<String> {
guard numberDictionary.keys.contains(key) else {return Opt()}
return Opt(numberDictionary[key]!)
}
Here's how I'm implementing flatMap(). The key, as it always is for flatMap is that the function already returns the type that we want.
extension Opt {
func flatMap<Output>(_ f: @escaping (Wrapped)
-> Opt<Output> )
-> Opt<Output> {
guard case .some(let value) = self else {return .none}
return f(value)
}
}
func >=><A, B>(a: Opt<A>,
f: @escaping (A) -> Opt<B>) -> Opt<B> {
return a.flatMap(f)
}
We use it the same as last time and get the same results.
echoEvenOrNil(2).flatMap(valueInNumberDictionary)
echoEvenOrNil(3).flatMap(valueInNumberDictionary)
echoEvenOrNil(4).flatMap(valueInNumberDictionary)
I asked a dumb question on Twitter this morning and @nicklockwood, @jesseMeow, and @jl_hfl were so nice in response that I'm adding the following material to this post.
Let's implement the nil-coalescing operator for our optional.
This turns out to be a handy device that says, "unwrap this guy and use it if it's not nil - and if it is nil, use this default value instead."
Let's start with the method version of this:
extension Opt {
func getOrElse(_ defaultValue: Wrapped) -> Wrapped {
switch self {
case .some(let value):
return value
default:
return defaultValue
}
}
}
We can use it like this.
echoEvenOrNil(3).getOrElse(-1)
echoEvenOrNil(2).getOrElse(-1)
This returns the default value -1 in the first case and 2 in the second.
We can overload the same symbol that Apple uses for the nil-coalescing operator: ??.
func ??<A>(opt: Opt<A>, defaultValue: A) -> A {
return opt.getOrElse(defaultValue)
}
We use it like this:
echoEvenOrNil(3) ?? -1
echoEvenOrNil(2) ?? -1
Pretty cool - huh?
OK, next time we'll look for a type that lives somewhere between throwing an error and our optional type. It is known as the either type in many languages or the result type.