Skip to content

Instantly share code, notes, and snippets.

@TizianoCoroneo
Last active April 10, 2019 09:09
Show Gist options
  • Save TizianoCoroneo/73aedea725176e649d1a8f6cabd23d95 to your computer and use it in GitHub Desktop.
Save TizianoCoroneo/73aedea725176e649d1a8f6cabd23d95 to your computer and use it in GitHub Desktop.
Type level Even or Odd in Swift 4.2
import Foundation
/*:
Run this in a Swift playground!
The goal is to have some kind of "even box" type that can contain only even numbers, and a "odd box" that can contain only odd numbers.
With that setup, it is possible to define some operations between "boxes" to check at compile time if the result of an expression will be even or odd.
*/
/**
A protocol in common for `Even<T>` and `Odd<T>`.
Every Even or Odd box should have its `value`, and a `name`: `"even"` or `"odd"`.
They also have a throwing init that throws an error in case we try to "lie" about what number we want to put in the box:
```
let x = try! Even(3) // Will throw `.valueIsNotEven(3)`
```
*/
protocol EvenOrOdd: CustomStringConvertible {
// Underlying number type. May be Int16, Int32, Int64, Int...
associatedtype T: FixedWidthInteger
// The type reference to the "Other" type of box.
// It should be `Odd<T>` if `Self == Even<T>`, should be `Even<T>` if `Self == Odd<T>`.
associatedtype Other: EvenOrOdd where Other.Other == Self, Other.T == T
/// `"even"` or `"odd"`?
var name: String { get }
/// Numeric value inside the box.
var value: T { get set }
/**
A throwing init that throws an error in case we try to "lie" about what number we want to put in the box:
```
let x = try! Even(3) // Will throw `.valueIsNotEven(3)`
```
*/
init(_ value: T) throws
}
extension EvenOrOdd {
/// Naming is hard but descripting is harder
var description: String {
return "\(value) is \(name)"
}
}
//: The next enum represents _everything that could go wrong_.
enum EvenOrOddErrors<T: FixedWidthInteger>: LocalizedError {
case valueIsNotEven(T)
case valueIsNotOdd(T)
}
/*:
Now let's start with actually defining the boxes!
Here's the `Even` box.
*/
struct Even<T: FixedWidthInteger>: EvenOrOdd {
typealias Other = Odd<T>
/// `"even"`!
var name: String { return "even" }
var value: T
init(_ value: T) throws {
guard value % 2 == 0
else { throw EvenOrOddErrors<T>.valueIsNotEven(value) }
self.value = value
}
}
//: And the Odd box.
struct Odd<T: FixedWidthInteger>: EvenOrOdd {
typealias Other = Even<T>
/// `"odd"`!
var name: String { return "odd" }
var value: T
init(_ value: T) throws {
guard value % 2 == 1
else { throw EvenOrOddErrors<T>.valueIsNotOdd(value) }
self.value = value
}
}
/*:
Let's test if everything goes as expected.
If you don't lie, you get a box.
If you lie, you get an exception.
*/
let _1 = try! Odd(1)
let _2 = try! Even(2)
let _3 = try! Odd(3)
let _4 = try! Even(4)
let _5 = try! Odd(5)
let _6 = try! Even(6)
let imNil = try? Even(5) // If you remove the `?` you can see the exception.
/*:
Now we define two utilities to manipulate boxes:
- the `bind` function (that should really be called "re-box")
- the `lift` function
*/
extension EvenOrOdd {
/**
Transorms a box in another.
This function takes the content of the box on which it is called, applies a function `f` that creates a new box to it, and returns that new box.
```
let x = try! Even(2) // x == Even(2)
let newBox = try! x.bind { try Odd($0 + 1) } // y == Odd(3)
```
*/
func bind<Box: EvenOrOdd>(_ f: (T) throws -> Box) rethrows -> Box
where Box.T == T {
return try f(value)
}
/**
Transforms an operation on numbers in an operation on boxes.
This functions takes a function that accepts two values and returns one value and turns it into a function that accepts two `EvenOrOdd` boxes, and return the type of box specified by the type that we call this function on (since it is a static function).
Example:
```
let newPlus: (Even<Int>, Even<Int>) throws -> Even<Int> = Even.lift(+)
```
Defining this `newPlus` functions establish the property that summing two `even` box produces always a `even` box.
*/
static func lift<S: EvenOrOdd, R: EvenOrOdd>(
_ f: @escaping (T, T) -> (T))
-> (S, R) throws -> Self
where R.T == T, S.T == T {
return { a, b in
try a.bind { av in
try b.bind { bv in
try Self.init(f(av, bv)) } } }
}
}
/*:
Addition has the following behavior:
- Even + Even = Even
- Even + Odd = Odd
- Odd + Even = Odd
- Odd + Odd = Even
If we add a `X` type of box with the same type, we always get `Even`.
If we add a `X` type of box with `X.Other`, we always get `Odd`.
This implementation of addition encodes this behavior with type constraints.
Every implementation has a set of type constraints that avoids ambiguity in which function to use.
*/
func +<Box: EvenOrOdd>(_ l: Box, _ r: Box) -> Even<Box.T> {
return try! Even.lift(+)(l, r)
}
func +<Box: EvenOrOdd>(_ l: Box.Other, _ r: Box) -> Odd<Box.T> {
return try! Odd.lift(+)(l, r)
}
/*:
Multiplication has the following behavior:
- Even * Even = Even
- Even * Odd = Even
- Odd * Even = Even
- Odd * Odd = Odd
If we multiply two `Even` boxes, we get `Even`.
If we multiply two different boxes, we get `Even`.
If we multiply two `Odd` boxes, we get `Odd`.
This implementation of addition encodes this behavior with type constraints.
Every implementation has a set of type constraints that avoids ambiguity in which function to use.
*/
func *<I>(_ l: Odd<I>, _ r: Odd<I>) -> Odd<I> {
return try! Odd<I>.lift(*)(l, r)
}
func *<S: EvenOrOdd>(_ l: S, _ r: S.Other) -> Even<S.T> {
return try! Even<S.T>.lift(*)(l, r)
}
func *<I>(_ l: Even<I>, _ r: Even<I>) -> Even<I> {
return try! Even<I>.lift(*)(l, r)
}
//: Have fun! 🍕
print(_2 * _4)
print(_1 * _3)
print(_2 * _3)
print(_1 * _4)
print(_2 + _4)
print(_1 + _3)
print(_2 + _3)
print(_1 + _4)
print((_1 + _4) * _2 + _3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment