Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type safety on Workflow Launch Args #22

Open
Tyler-Keith-Thompson opened this issue May 12, 2021 · 4 comments
Open

Type safety on Workflow Launch Args #22

Tyler-Keith-Thompson opened this issue May 12, 2021 · 4 comments
Labels
enhancement New feature or request low priority This is not a current focus

Comments

@Tyler-Keith-Thompson
Copy link
Collaborator

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.

@Tyler-Keith-Thompson Tyler-Keith-Thompson added the enhancement New feature or request label May 12, 2021
@Tyler-Keith-Thompson
Copy link
Collaborator Author

Tyler-Keith-Thompson commented May 25, 2021

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?

@Richard-Gist Richard-Gist added the low priority This is not a current focus label Jun 14, 2021
@morganzellers
Copy link
Contributor

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

@Tyler-Keith-Thompson
Copy link
Collaborator Author

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.

@Richard-Gist
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request low priority This is not a current focus
Projects
None yet
Development

No branches or pull requests

3 participants