Skip to content

Instantly share code, notes, and snippets.

@jemmons
Created March 23, 2021 21:10
Show Gist options
  • Save jemmons/6d34d75c0dc1f47dbf7f248cd210d7ea to your computer and use it in GitHub Desktop.
Save jemmons/6d34d75c0dc1f47dbf7f248cd210d7ea to your computer and use it in GitHub Desktop.
Continuation Monad in Swift
import Foundation
//: Consider traditional Cocoa async calls with completion blocks like:
func prependHello(with i: Int, completion: (String) -> Void) {
completion("hello \(i)")
}
//: We could imagine this to be the CPS version of some standard call:
func preprensHelloSync(with i: Int) -> String {
return "hello \(i)"
}
//: In other words, completion blocks are functioning as continuations (no big surprise. Just different names for the same thing). We can define them like so:
typealias Continuation<T> = (T) -> Void
//: Given a definition for a continuation, we can define the Continuation Monad:
typealias ContinuationMonad<T> = (@escaping Continuation<T>) -> Void
//: Which is just a function that take a completion and calls it with its wrapped value.
func unit<T>(_ wrappedValue: T) -> ContinuationMonad<T> {
return { continuation in
continuation(wrappedValue)
}
}
//: It's a monad, so we need a flatMap. It must take a transform from the wrapped value to a new monad. We'll define a type for this, too, to keep Swift happy:
typealias ContinuationMonadTransform<T,U> = (T) -> ContinuationMonad<U>
func flatMap<T,U>(_ monad: @escaping ContinuationMonad<T>, _ transform: @escaping ContinuationMonadTransform<T,U>) -> ContinuationMonad<U> {
return { continuation in
monad { wrappedValue in
transform(wrappedValue)(continuation)
}
}
}
//: Now, that transform type has a pretty janky shape (`(T)->((U)->Void)->Void`). But it just happens to be exactly what you get if you curry the first arg off our async func above!
public func cury<T, U, Z>(_ ƒ: @escaping (T, U)->Z) -> (T) -> (U) -> Z {
return { t in { u in ƒ(t, u) } }
}
flatMap(unit(42), cury(prependHello))
//: Which means, with an operator, we can chain our async funcs without nesting:
precedencegroup BindOperator { associativity: left }
infix operator >>=: BindOperator
func >>=<T,U> (lhs: @escaping ContinuationMonad<T>, rhs: @escaping ContinuationMonadTransform<T,U>) -> ContinuationMonad<U> {
return flatMap(lhs, rhs)
}
func addTwo(to i: Int, completion: (Int)->Void) {
completion(i + 2)
}
unit(42)
>>= cury(addTwo)
>>= cury(prependHello)
//: to actually have this do anything, of course, we need to pass a continuation to the resultant monad to run. We can do that with a simple closure:
(unit(42)
>>= cury(addTwo)
>>= cury(prependHello)) { print($0) }
//: But then consider a (very jank) implementation of call/cc. This just passes the "current continuation" we give it to called by the scope (that we also give it) at a later time:
func callCC<T>(_ scope: (@escaping Continuation<T>)->Void, cc: @escaping Continuation<T>) {
scope(cc)
}
callCC({ cc in
cc("hello!")
}) { print($0) }
//: But then note that the `(Continuation<T>)->Void` type of the `scope` just happens to be the same as the Continuation Monad! So instead of composing a closure, we can give it our chain of monads.
callCC(
unit(42)
>>= cury(addTwo)
>>= cury(prependHello)
) { returning in
print(returning)
}
//: Which lets us succinctly call a chain of async functions only passing a completion for the last one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment