Skip to content

Instantly share code, notes, and snippets.

@alskipp
Last active November 14, 2015 09:12
Show Gist options
  • Save alskipp/779b4497e72a8d1c72f2 to your computer and use it in GitHub Desktop.
Save alskipp/779b4497e72a8d1c72f2 to your computer and use it in GitHub Desktop.
/*
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.
*/
/* 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.
@alskipp
Copy link
Author

alskipp commented Feb 19, 2015

This is a follow up to a talk I gave at Swift London (https://twitter.com/SwiftLDN) about 'Swift and Nothingness'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment