Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An implementation of the catamorphism recursion scheme in Swift
// NOTE: This code was written when I first figured out how to encode HKT in Swift.
// There is a lot that can be improved and I would write it somewhat differently today.
// This example shows how higher-kinded types can be emulated in Swift today.
// It acheives correct typing at the cost of some boilerplate, manual lifting and an existential representation.
// The technique below was directly inspired by the paper Lightweight Higher-Kinded Polymorphism
// by Jeremy Yallop and Leo White found at http://ocamllabs.io/higher/lightweight-higher-kinded-polymorphism.pdf
/// `ConstructorTag` represents a type constructor.
/// `Argument` represents an argument to the type constructor.
struct Apply<ConstructorTag, Argument> {
/// An existential containing a value of `Constructor<Argument>`
/// Where `Constructor` is the type constructor represented by `ConstructorTag`
let tag: ConstructorTag
}
protocol TypeConstructor2 {
associatedtype Tag
associatedtype Argument1
associatedtype Argument2
typealias Applied = Self
}
protocol Apply2Protocol {
associatedtype Tag
associatedtype Argument1
associatedtype Argument2
//associatedtype Applied
}
protocol Apply2Tag {
associatedtype Tag
// This has arbitrary type arguments. It is only used as an argument to Replace2
associatedtype Applied: TypeConstructor2 where Applied.Tag == Self
}
struct Apply2<ConstructorTag: Apply2Tag, Arg1, Arg2>: Apply2Protocol {
/// An existential containing a value of `Constructor<Argument1, Argument2>`
/// Where `Constructor` is the type constructor represented by `ConstructorTag`
let tag: ConstructorTag
typealias Tag = ConstructorTag
typealias Argument1 = Arg1
typealias Argument2 = Arg2
typealias Applied = ConstructorTag.Applied
}
protocol Replace2Protocol {
associatedtype Constructor: TypeConstructor2
associatedtype Argument1
associatedtype Argument2
associatedtype Replaced: TypeConstructor2 where Replaced.Tag == Constructor.Tag, Replaced.Argument1 == Argument1, Replaced.Argument2 == Argument2
}
struct ArrayReplacer<A: TypeConstructor2, Argument> where A.Tag == ArrayTag {
typealias Replaced = [Argument]
}
/// A protocol all type constructors must conform to.
protocol TypeConstructor {
/// The existential type that erases `Argument`.
/// This should only be initializable with values of types created by the current constructor.
associatedtype Tag
/// The argument that is currently applied to the type constructor in `Self`.
associatedtype Argument
/// `self` stored in the Tag existential
var apply: Apply<Tag, Argument> { get }
/// Must unwrap the `app.tag` existential.
static func unapply(_ apply: Apply<Tag, Argument>) -> Self
}
struct ArrayTag {
fileprivate let array: Any
// Private access to the initializer is what makes this a safe technique.
// Creating an `Apply` (where the type information is stored)
// requires creating a `Tag` first.
// Using access control we can restrict that to the same file that defines
// the `Array: TypeConstructor` conformance below to ensure that
// `Apply<ArrayTag, T>` instances are only created with the correct type of
// array values.
init<T>(_ array: [T]) {
self.array = array
}
}
extension Array: TypeConstructor {
typealias Tag = ArrayTag
var apply: Apply<Tag, Element> {
return Apply<Tag, Element>(tag: ArrayTag(self))
}
static func unapply(_ apply: Apply<Tag, Element>) -> Array {
return apply.tag.array as! Array
}
}
struct OptionalTag {
fileprivate let optional: Any
init<T>(_ optional: T?) {
self.optional = optional as Any
}
}
extension Optional: TypeConstructor {
typealias Tag = OptionalTag
var apply: Apply<Tag, Wrapped> {
return Apply<Tag, Wrapped>(tag: OptionalTag(self))
}
static func unapply(_ apply: Apply<Tag, Wrapped>) -> Optional {
return apply.tag.optional as? Wrapped
}
}
protocol Functor: TypeConstructor {
func map<T>(_ transform: (Argument) -> T) -> Apply<Tag, T>
}
protocol Monad: TypeConstructor {
static func wrap<T>(_ value: T) -> Apply<Tag, T>
func flatMap<T>(_ continuation: (Argument) -> Apply<Tag, T>) -> Apply<Tag, T>
}
extension Array: Monad {
static func wrap<T>(_ value: T) -> Apply<Tag, T> {
return [value].apply
}
func flatMap<T>(_ continuation: (Element) -> Apply<Tag, T>) -> Apply<Tag, T> {
return flatMap { [T].unapply(continuation($0)) }.apply
}
}
extension Optional: Monad {
static func wrap<T>(_ value: T) -> Apply<Tag, T> {
return (value as T?).apply
}
func flatMap<T>(_ continuation: (Wrapped) -> Apply<Tag, T>) -> Apply<Tag, T> {
return flatMap { T?.unapply(continuation($0)) }.apply
}
}
// Here we use flatMap on values of types [Int] and Int?.
// The result is automatically lifted into the corresponding emulated HKT.
// [1, 2, 3, 2, 4, 6, 3, 6, 9, 4, 8, 12]
Array.unapply([1, 2, 3, 4].flatMap { [$0, $0 * 2, $0 * 3].apply })
Optional.unapply((42 as Int?).flatMap { (($0 * 2) as Int?).apply }) // 84
Optional.unapply((nil as Int?).flatMap { _ in (nil as Int?).apply }) // nil
protocol FunctorTag {
static func map<T, U>(_ value: Apply<Self, T>, _ transform: (T) -> U) -> Apply<Self, U>
}
protocol MonadTag {
static func wrap<T>(_ value: T) -> Apply<Self, T>
static func flatMap<T, U>(_ value: Apply<Self, T>, _ continuation: (T) -> Apply<Self, U>) -> Apply<Self, U>
}
extension ArrayTag: MonadTag {
static func wrap<T>(_ value: T) -> Apply<ArrayTag, T> {
return [value].apply
}
static func flatMap<T, U>(_ value: Apply<ArrayTag, T>, _ continuation: (T) -> Apply<ArrayTag, U>) -> Apply<ArrayTag, U> {
return Array.unapply(value).flatMap(continuation)
}
}
extension OptionalTag: MonadTag {
static func wrap<T>(_ value: T) -> Apply<OptionalTag, T> {
return (value as T?).apply
}
static func flatMap<T, U>(_ value: Apply<OptionalTag, T>, _ continuation: (T) -> Apply<OptionalTag, U>) -> Apply<OptionalTag, U> {
return Optional.unapply(value).flatMap(continuation)
}
}
/// We will soon be able to declare conformances for the emulated HKT existentials themselves!
extension Apply/*: Monad */ where ConstructorTag: MonadTag {
static func wrap<T>(_ value: T) -> Apply<ConstructorTag, T> {
return ConstructorTag.wrap(value)
}
func flatMap<T>(_ continuation: (Argument) -> Apply<ConstructorTag, T>) -> Apply<ConstructorTag, T> {
return ConstructorTag.flatMap(self, continuation)
}
}
// Here we use flatMap directly on the emulated HKT values of types Apply<ArrayTag, Int>
// and Array<OptionalTag, Int> and observe the same results as flatMap applied to the base types.
// [1, 2, 3, 2, 4, 6, 3, 6, 9, 4, 8, 12]
Array.unapply([1, 2, 3, 4].apply.flatMap { [$0, $0 * 2, $0 * 3].apply })
Optional.unapply((42 as Int?).apply.flatMap { (($0 * 2) as Int?).apply }) // 84
Optional.unapply((nil as Int?).apply.flatMap { _ in (nil as Int?).apply }) // nil
protocol NaturalTransformation {
associatedtype FromTag
associatedtype ToTag
static func apply<T>(to value: Apply<FromTag, T>) -> Apply<ToTag, T>
}
// A natural transformation from T? to [t]
enum OptionalToArray: NaturalTransformation {
static func apply<T>(to optional: Apply<OptionalTag, T>) -> Apply<ArrayTag, T> {
return [Optional.unapply(optional)].flatMap { $0 }.apply
}
}
extension Apply {
func transform<Transformation: NaturalTransformation>(using transformation: Transformation.Type) -> Apply<Transformation.ToTag, Argument> where Transformation.FromTag == ConstructorTag {
return Transformation.apply(to: self)
}
}
// Apply the natural transformation to values of the emulated HKT type Apply<OptionalTag, Int>
// to receive values of emulated HKT type Apply<ArrayTag, Int> and then unwrap the result.
Array.unapply((42 as Int?).apply.transform(using: OptionalToArray.self)) // [42]
Array.unapply((nil as Int?).apply.transform(using: OptionalToArray.self)) // []
struct Fix<ConstructorTag> {
var unfix: Apply<ConstructorTag, Fix<ConstructorTag>>
init(_ value: Apply<ConstructorTag, Fix<ConstructorTag>>) {
unfix = value
}
}
struct ExprTag {
fileprivate let expr: Any
init<T>(_ expr: Expr<T>) {
self.expr = expr
}
}
enum Expr<T> {
case int(Int)
indirect case add(T, T)
indirect case mul(T, T)
}
extension Expr: TypeConstructor {
typealias Tag = ExprTag
var apply: Apply<ExprTag, T> {
return Apply(tag: ExprTag(self))
}
static func unapply(_ apply: Apply<ExprTag, T>) -> Expr {
return apply.tag.expr as! Expr
}
}
extension Apply where ConstructorTag == ExprTag {
static func int(_ i: Int) -> Apply {
return Expr<Argument>.int(i).apply
}
static func add(_ lhs: Argument, _ rhs: Argument) -> Apply {
return Expr<Argument>.add(lhs, rhs).apply
}
static func mul(_ lhs: Argument, _ rhs: Argument) -> Apply {
return Expr<Argument>.mul(lhs, rhs).apply
}
}
extension Expr: Functor {
func map<U>(_ transform: (T) -> U) -> Apply<ExprTag, U> {
switch self {
case .int(let i): return Expr<U>.int(i).apply
case .add(let lhs, let rhs):
return Expr<U>.add(transform(lhs), transform(rhs)).apply
case .mul(let lhs, let rhs):
return Expr<U>.mul(transform(lhs), transform(rhs)).apply
}
}
}
extension ExprTag: FunctorTag {
static func map<T, U>(_ value: Apply<ExprTag, T>, _ transform: (T) -> U) -> Apply<ExprTag, U> {
return Expr<T>.unapply(value).map(transform)
}
}
extension Expr where T == Int {
var eval: T {
switch self {
case .int(let val): return val
case .add(let lhs, let rhs): return lhs + rhs
case .mul(let lhs, let rhs): return lhs * rhs
}
}
}
extension Apply where ConstructorTag == ExprTag, Argument == Int {
var eval: Argument {
return Expr<Int>.unapply(self).eval
}
}
func cata<Tag: FunctorTag, R, A>(
_ fAlgebra: @escaping (Apply<Tag, A>) -> A,
_ out: @escaping (R) -> Apply<Tag, R>,
_ value: R
) -> A {
return fAlgebra(Tag.map(out(value)) { cata(fAlgebra, out, $0 ) })
}
func evalAlgebra(_ expr: Apply<ExprTag, Int>) -> Int {
return Expr<Int>.unapply(expr).eval
}
func out(_ fix: Fix<ExprTag>) -> Apply<ExprTag, Fix<ExprTag>> {
return fix.unfix
}
func eval(_ fix: Fix<ExprTag>) -> Int {
return cata(evalAlgebra, out, fix)
}
func cataFix<Tag: FunctorTag, A>(
_ value: Fix<Tag>,
_ fAlgebra: @escaping (Apply<Tag, A>) -> A
) -> A {
return fAlgebra(Tag.map(value.unfix) { cataFix($0, fAlgebra) })
}
let evalFix = { cataFix($0) { Expr<Int>.unapply($0).eval }} // $0.unapply.eval
let expr = Fix<ExprTag>(.mul(
Fix<ExprTag>(.int(42)),
Fix<ExprTag>(.add(
Fix<ExprTag>(.int(42)),
Fix<ExprTag>(.int(42))
))))
let evaled = eval(expr)
let evaledFix = evalFix(expr)
let val = 42 * (42+42)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.