Found an even more general "solution", but it's not exactly type safe. One can write some boilerplate though to make it "kinda" safe...
public struct Free<Meta, T> {
let kind : Kind
enum Kind {
case pure(T)
case free(Meta, (Any) -> Free<Meta, T>)
}
private static func pure(_ t: T) -> Self {
.init(kind: .pure(t))
}
public static func free(_ meta: Meta, _ trafo: @escaping (Any) -> Free<Meta, T>) -> Self {
.init(kind: .free(meta, trafo))
}
static func lift(_ meta: Meta) -> Self {
.free(meta) {any in .pure(any as! T)}
}
}
public extension Free {
func erased() -> Free<Meta, Any> {
flatMap{.pure($0)}
}
func map<U>(_ trafo: @escaping (T) -> U) -> Free<Meta, U> {
flatMap{arg in .pure(trafo(arg))}
}
func flatMap<U>(_ trafo: @escaping (T) -> Free<Meta, U>) -> Free<Meta, U> {
switch kind {
case .pure(let t):
return trafo(t)
case .free(let free, let trf):
return .free(free) {any in trf(any).flatMap(trafo)}
}
}
func parseRec<NewMeta>(_ rec: @escaping (Free<Meta, T>) -> Free<NewMeta, T>,
_ trafo: @escaping (Meta) -> Free<NewMeta, Any>) -> Free<NewMeta, T> {
switch kind {
case .pure(let t):
return .pure(t)
case .free(let meta, let trf):
return trafo(meta).flatMap{any in trf(any).parseRec(rec, trafo)}
}
}
func runUnsafe(_ exec: (Meta) async throws -> Any) async rethrows -> T {
var this = kind
while true {
switch this {
case .pure(let t):
return t
case .free(let meta, let trafo):
this = try await trafo(exec(meta)).kind
}
}
}
}
Given the following enum:
enum Console {
case print(String)
case readLn
}
I wonder if we could somehow have a macro that generates the following (or similar) code:
extension Free {
static func print(_ arg: String) -> Free<Console, Void> where Meta == Console, T == Void {
.lift(.print(arg))
}
static func readLn() -> Free<Console, String> where Meta == Console, T == String {
.lift(.readLn)
}
}
extension Free where Meta == Console {
func parse<M>(parsePrint: @escaping (String) -> Free<M, Void>,
parseReadLn: @escaping () -> Free<M, String>) -> Free<M, T> {
parseRec({$0.parse(parsePrint: parsePrint, parseReadLn: parseReadLn)}) {meta in
switch meta {
case .print(let string):
parsePrint(string).erased()
case .readLn:
parseReadLn().erased()
}
}
}
func runUnsafe(onPrint: (String) -> Void,
onRead: () -> String) async -> T {
await runUnsafe{meta in
switch meta {
case .print(let string):
return onPrint(string)
case .readLn:
return onRead()
}
}
}
}