Last active
November 14, 2015 09:12
-
-
Save alskipp/779b4497e72a8d1c72f2 to your computer and use it in GitHub Desktop.
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
/* | |
This is a demonstration of how to implement the Optional type in Swift. | |
The name 'Maybe' is taken from Haskell, but the two cases 'None' & 'Some' | |
are the same as Swift's Optional type (Haskell uses 'Nothing' & 'Just'). | |
The Maybe type conforms to NilLiteralConvertible, as does Swift's | |
Optional type. This allows a Maybe value to be constructed from 'nil'. | |
One aspect of Swift's Optional type which can't be reproduced is | |
'implicit Optional wrapping'. Here's an example: | |
.None < 0 | |
Such a comparison should not be possible as the types don't match. | |
However, Swift will automatically convert the expression to: | |
.None < .Some(0) | |
It's impossible to reproduce the recipe for this secret sauce – it's an | |
Optional only capability which is often convenient, sometimes bizarre. | |
'Implicit Optional wrapping' also comes into play when returning Optional | |
values from functions. When reimplementing Optionals, values must always | |
be explicitly wrapped using 'Maybe(x)', or by using the '.Some(x)' constructor. | |
*/ | |
enum Maybe<T> : NilLiteralConvertible { | |
case None, Some(T) | |
init() { self = None } // init with no args defaults to 'None' | |
init(_ some: T) { self = Some(some) } | |
init(nilLiteral: ()) { self = None } // init with 'nil' defaults to 'None' | |
func map<U>(f: T -> U) -> Maybe<U> { | |
switch self { | |
case .None : return .None | |
case .Some(let x) : return .Some(f(x)) | |
} | |
} | |
} | |
extension Maybe : Printable { | |
var description: String { | |
switch self { | |
case .None : return "{None}" | |
case .Some(let x) : return "{Some \(x)}" | |
} | |
} | |
} | |
/* | |
The built in Optional type does not conform to the Equatable or Comparable protocols. | |
Conforming to these protocols would prevent non-comparable values from being declared Optional. | |
This is what the type restriction would look like: | |
enum Optional<T:Comparable> {} | |
There are however overloaded operators for equality and comparison that accept Optionals. | |
Below are a few overloaded operators for the Maybe type. As we can't conform to Comparable | |
we don't get any operators for free : ( | |
*/ | |
func == <T: Equatable>(lhs: Maybe<T>, rhs: Maybe<T>) -> Bool { | |
switch (lhs, rhs) { | |
case (.None, .None) : return true | |
case (.Some(let x), .Some(let y)) : return x == y | |
default : return false | |
} | |
} | |
func < <T: Comparable>(lhs: Maybe<T>, rhs: Maybe<T>) -> Bool { | |
switch (lhs, rhs) { | |
case (.None, .Some) : return true | |
case (.Some(let x), .Some(let y)) : return x < y | |
default : return false | |
} | |
} | |
func > <T: Comparable>(lhs: Maybe<T>, rhs: Maybe<T>) -> Bool { | |
return rhs < lhs | |
} | |
// initializing Maybe without an arg requires a type declaration | |
let m = Maybe<Int>() | |
println(m) // {None} | |
// initializing Maybe with an arg – the type is inferred from the arg | |
let m1 = Maybe(1) | |
println(m1) // {Some 1} | |
// map func returns a new Maybe | |
let m2 = m1.map { $0 + 1 } | |
println(m2) // {Some 2} | |
m1 == m2 | |
m1 < m2 | |
m1 > m2 | |
nil < m1 // NilLiteralConvertible is invoked to contruct a Maybe value from 'nil' | |
// Example of, 'implicit Optional wrapping' with Optionals | |
Optional(1) < 2 | |
// the equivalent code using Maybe will not compile | |
// Maybe(1) < 2 | |
/* | |
Compared to the built in Optional type, the lack of syntax sugar will make the | |
Maybe type a bitter pill to swallow. Optional chaining with '?', nope. | |
Unwrapping multiple Maybes with 'if let' syntax? No chance. | |
*/ |
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
/* Life without Optional sugar!! | |
What would it look like to use the Maybe type instead of the built in Optional type? | |
It might not be obvious, but the syntax sugar we use with Optionals (Optional chaining '?') | |
is in fact a monadic bind operation. | |
Consequently, we need to introduce monadic bind to the Maybe type (sugar free). Hold on! | |
*/ | |
// 'Lift' plain values into the Maybe type. | |
// It's the explicit equivalent of 'implicit Optional wrapping'. | |
func pure<A>(x:A) -> Maybe<A> { | |
return Maybe(x) | |
} | |
// Monadic bind operator for the Maybe type. | |
// This will be used instead of optional chaining syntax (a?.b?.c) | |
infix operator >>== {associativity left} | |
func >>== <A,B> (m: Maybe<A>, f: A -> Maybe<B>) -> Maybe<B> { | |
switch m { | |
case .None : return .None | |
case .Some(let m) : return f(m) | |
} | |
} | |
/* | |
To put our binding operator through its paces, let's create a few structs with properties to work on. | |
The idea is we have a Room struct with 'width' & 'length' properties. | |
We have a Residence struct with an Array of Rooms. | |
Finally we have a Person with a 'name' and a Maybe<Residence> | |
So, a Person, may or may not have a Residence. A Residence has an Array of rooms. | |
A Room has a width and length. Given these parameters we want to write a function | |
that calculates the livingspace of a person. The return value of this function | |
will be Maybe<Int>, because a Person may not have a Residence. | |
*/ | |
struct Room { let length:Int, width:Int } | |
struct Residence { let rooms:[Room] } | |
struct Person { let name:String, residence:Maybe<Residence>} | |
let bob = Person(name: "Bob", residence: .None) | |
let jo = Person(name: "Jo", | |
residence: .Some( | |
Residence(rooms: [Room(length: 4, width: 3), Room(length: 2, width: 2)]) | |
) | |
) | |
func livingSpace(person:Person) -> Maybe<Int> { | |
return person.residence >>== | |
{ pure($0.rooms) } >>== | |
{ pure($0.map {r in r.length * r.width}) } >>== | |
{ pure(reduce($0, 0, +)) } | |
} | |
let bob_space = livingSpace(bob) | |
println(bob_space) // {None} | |
let jo_space = livingSpace(jo) | |
println(jo_space) // {Some 16} | |
/* | |
As a comparison, here's how it would look using the built in Optional type. | |
First, we need to declare a new version of the Person struct that has an Optional Residence. | |
The implementation should be much more familiar and easier to follow. | |
Just remember, the slightly crazy looking stuff you see above in the Maybe type version, | |
it's all still happening, but concealed beneath the syntax sugar of Optional chaining. | |
*/ | |
struct Person2 { let name:String, residence:Residence?} | |
let jed = Person2(name: "Jed", residence: .None) | |
let fi = Person2(name: "Fi", | |
residence: Residence(rooms: [Room(length: 4, width: 3), Room(length: 2, width: 2)])) | |
func livingSpace(person:Person2) -> Int? { | |
return person.residence?.rooms.map {$0.length * $0.width }.reduce(0, combine:+) | |
} | |
let jed_space = livingSpace(jed) // nil (Or to be precise .None) | |
let fi_space = livingSpace(fi) // Some(16) - but Xcode will just state 16, because it's mendacious. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is a follow up to a talk I gave at Swift London (https://twitter.com/SwiftLDN) about 'Swift and Nothingness'.