Created
March 23, 2021 21:10
-
-
Save jemmons/6d34d75c0dc1f47dbf7f248cd210d7ea to your computer and use it in GitHub Desktop.
Continuation Monad in Swift
This file contains 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
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