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 Workflow
s 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.