wwt/SwiftCurrent

Type safety on Workflow Launch Args

Tyler-Keith-Thompson opened this issue · 4 comments

Is your feature request related to a problem? Please describe.

It's unfortunate that when I launch a Workflow the arguments I pass to launch it are not typed. So I don't get a compiler check.

Describe the solution you'd like

The short version? I want launch to be smart enough to know what the first item in the Workflow expects for its WorkflowInput and have that type be what I pass as args. Additionally if that type is Never I want to be forbidden from passing args and if that type is not Never I want to be forced to pass args.

Potential solution:
Combine has Publishers that end up chaining in an interesting way with Generics that we could rip off. For example

Workflow(FR1.self) creates a Workflow. But Workflow(FR1.self).thenProceed(with: FR2.self) creates Workflow<FR2, Workflow<FR1>> or...something like that? The solution is not totally ironed out in my head, but the premise is that you'd be able to backtrack through all the nested Workflows until you got to the first one, thus knowing the FlowRepresentable type that comes first and asserting launch happens with the correct input.

Describe alternatives you've considered

I suppose there's a simpler approach that is a little less consumer friendly where they just pass the FlowRepresentable type that is going to be launched and we steal the WorkflowInput from it. I'm not really a fan because it adds an unneeded parameter.

This is an absolutely insane example but it showcases a little of how this might be able to work. It's heavily inspired by Combine and not quite right still, but I think it demonstrates how this could be accomplished at all.

import Foundation

protocol PThing {
    associatedtype Upstream = Never
    associatedtype T
}

enum AllThings {

}

extension PThing {
    func then<V>(_ v: V) -> AllThings.CThing<Self, V> {
        AllThings.CThing<Self, V>(v)
    }

    func firstType<U>() -> U.T.Type where U: PThing, U == Upstream {
        U.T.self
    }

    func firstType() -> T.Type where Upstream == Never {
        T.self
    }
}

extension AllThings {
    class CThing<Upstream, T>: PThing {
        typealias Upstream = Upstream
        typealias T = T
        init(_ t: T) { }
    }
}

class Thing<T>: PThing {
    typealias Upstream = Never
    init(_ t: T) { }
    func then<PT: PThing>(_ pt: PT) -> PT where PT.Upstream == Upstream {
        return pt
    }
}
var a = Thing<String>("")
// NOTE: The complex type below is AllThings.CThing<Thing<String>, Bool>
print(a.then(true).firstType()) // prints "String"

Now imagine extending this example to the Workflow class. In this somewhat crude example T would additionally be constrained so that it had to be FlowRepresentable which is accomplished in the protocol declaration:

protocol PThing where T: FlowRepresentable {
    associatedtype Upstream = Never
    associatedtype T
}

Then when you call launch it uses the same kind of logic that lives in firstType() to backtrack through the stack till it finds what the first FlowRepresentable declared as its input type.

This example however is incomplete for several reasons:

  • it only backtracks one level up, so a.then(true).then(1) unfortunately prints "Bool" not "String" like we'd want
  • having a function that returns the firstType() is fundamentally wrong because you can't use that in a function signature like launch
  • This example does not remotely prove this feature is achievable, it merely hints that it might be if it tries hard enough
  • It may??? be possible to pull the same kind of stunt that retry uses in combine to restart a chain to find the correct type to start with
  • It may??? be possible to rip off a bit of how AnyPublisher works under the covers to do the same kind of type erasure but in the other direction?

This seems like a great idea, not sure when it will be a focus for us, but we're keeping an eye on it! 👍🏻

Update: The SwiftUI approach we've taken (or even alternative paths we've considered) tends to give us this, the issue isn't all the way closed since it'd be nice to see it in UIKit too.

I feel like this might be part of a bigger issue request to convert workflow UIKit to the more fluent API in SwiftUI. I'm thinking the change would be breaking though so we may want to batch a couple of changes together and do it all at once.