-
Notifications
You must be signed in to change notification settings - Fork 69
Add Transact*Task api's to the F# decider abstraction #375
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
Conversation
src/Equinox/Decider.fs
Outdated
| /// 2. (if events yielded) Attempt to sync the yielded events to the stream. | ||
| /// (Restarts up to <c>maxAttempts</c> times with updated state per attempt, throwing <c>MaxResyncsExhaustedException</c> on failure of final attempt.) | ||
| /// 3. Uses <c>render</c> to generate a 'view from the persisted final state | ||
| member _.TransactTask(interpret : 'state -> CancellationToken -> Task<'event list>, render : 'state -> 'view, ?load, ?attempts, ?ct) : Task<'view> = task { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you experimented with other signatures? e.g. would calling it TransactAsync work or does it mess up intellisense? (I can see the reasoning about giving specific names - we can always add aliases later but you can't undo an overloaded name as easily)
is the list -> seq hop still valuable (IIRC in the async case, it mattered somewhere at some time, but that might have been an old compiler/VS toolset on net45 a long time ago)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also wondering whether exposing a lower level TransactExAsync (or a Property exposing inner) might allow this overall need to be served by adding extension methods/type augmentations locally vs requiring every impl to be a PR and release?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also wondering are any of these signatures useful in dotnet-templates?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if it counts but there are a fair few Propulsion.Internal.Async.startImmediateAsTask in dotnet-templates that I believe could be (with some additional changes) removed with this change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you experimented with other signatures?
I'll admit that I didn't, I've been knee deep in OCaml (which doesn't support any overloading whatsoever) for the past few days so it didn't even occur to me to overload.
If it is possible would that be your preference?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the list -> seq hop still valuable
I'm not sure to be honest, I assumed it was there intentionally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The root issue here is that we need to know if there are events pretty much straight away in https://github.com/jet/equinox/blob/master/src/Equinox/Core.fs#L44
That was once boring F# async code, and list made sense - pattern matching etc
Recently for C#/DeciderCore, I made that be Task<_, 'res * 'e seq> as a LCD between C# yield returns (which IIRC can't be used to build arrays neatly?) and the lists from F#
I suspect that changing the list to seq will trigger some compiler errors in F# consumption in some relatively exotic cases. (I think I validated that, but not 100% sure)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't a list already a seq? can't we just pass the list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is. There are exotic cases (e.g. >> etc) wherein things sometimes break down though. They are definitely pretty rare and/or arguably shortcomings in type inference and/or type generalization
src/Equinox/Decider.fs
Outdated
| /// 2. (if events yielded) Attempt to sync the yielded events to the stream. | ||
| /// (Restarts up to <c>maxAttempts</c> times with updated state per attempt, throwing <c>MaxResyncsExhaustedException</c> on failure of final attempt.) | ||
| /// 3. Yield result | ||
| member _.TransactTask(decide : 'state -> CancellationToken -> Task<'result * 'event list>, ?load, ?attempts, ?ct) : Task<'result> = task { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would you think about forcing decide to return struct tuples? and maybe seq ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
decideto return struct tuples?
I'd love it if F# removed the need to add the struct keyword from the calling side (does it do that yet?). I've noticed the struct tuple / tuple divide cause confusion in my team at least. Though yes, I do agree that a struct tuple is appropriate here. I'm torn 🤔
and maybe
seq?
I think that'd go against the programming model. We encourage users to set up deciders in a certain way that uses lists, and as such I don't think moving the seq transformation to the client provides a lot of value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, decide functions don't get explicit type specs in source code (and I'm absolutely not adding type aliases!)
This means that the typical code on the client side will generate obj tuples unless people go typing struct
Having said that, returning a 'result and an event list is not the most common thing so other considerations can drive it
It's important to me that this final V4 API not look stupid in the context of normal F# code in 3 years time, even if that makes it quirky in the short term. I am unaware of (and don't expect) any incoming way to flip to struct tuples as a default (some funny ,, operator?!) though, which makes me think it needs to remain obj tuples.
My thinking was to leave the 'idiomatic' interface using Async+obj tuples+FSharp list due to a) it being idiomatic F# b) perf not mattering coz IO c) wanting continuation token propagation
If the CT is being passed, I guess requiring a struct tuple for the result is not a lot to ask though (esp if the outer ct becomes mandatory?)
| /// (Restarts up to <c>maxAttempts</c> times with updated state per attempt, throwing <c>MaxResyncsExhaustedException</c> on failure of final attempt.) | ||
| /// 3. Yields a final 'view produced by <c>mapResult</c> from the <c>'result</c> and/or the final persisted <c>ISyncContext</c> | ||
| member _.TransactExTask(decide : ISyncContext<'state> -> CancellationToken -> Task<'result * 'event list>, mapResult : 'result -> ISyncContext<'state> -> 'view, | ||
| ?load, ?attempts, ?ct) : Task<'view> = task { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove 2 spaces ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😂, even my fancy vim indent config can't handle your demands!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a pile of = sequences [already in there], can you clean them too while you're a it please?
Also for avoidance of doubt, the ?load being out of alignment is the reason I commented here
|
The Task vs Async naming feels a bit wonky on first look, even though it's following the logic of the naming in general. Other questions spring to mind:
I do appreciate the overall idea of removing technical noise from business logic of course, but perhaps having some helpers in a ubiquitous lower level shims lib might address that more generally e.g. fsprojects/FSharp.Control.TaskSeq#142 |
|
With the latest changes, I wonder whether doing a Are you using something like IcedTasks for |
The next RC of Propulsion will provide a lower level API based on Tasks, Funcs and struct tuples For this release, I'm going to expose a As discussed out of band, the key issue with the APIs in DeciderCore from a F# perspective is that they operate on I'm hopeful that taking a bit longer before baking something noisy in will ultimately yield a better overall result, even if it's not entirely clear to either of us what that might actually become. |
We're moving towards using Tasks more and more and this API would be helpful in situations such as calling deciders from propulsion as propulsion handlers are now Task based while deciders are still Async.