Replies: 36 comments
-
@TimLariviere's debounce code in ElmishContacts/ElmishExtensions.fs is a nice example of using namespace ElmishContacts
open Fabulous.Core
open System.Collections.Generic
open System.Threading
module Cmd =
let ofAsyncMsgOption (p: Async<'msg option>) : Cmd<'msg> =
[ fun dispatch -> async { let! msg = p in match msg with None -> () | Some msg -> dispatch msg } |> Async.StartImmediate ]
module Extensions =
let debounce<'T> =
let memoizations = Dictionary<obj, CancellationTokenSource>(HashIdentity.Structural)
fun (timeout: int) (fn: 'T -> unit) value ->
let key = fn.GetType()
// Cancel previous debouncer
match memoizations.TryGetValue(key) with
| true, cts -> cts.Cancel()
| _ -> ()
// Create a new cancellation token and memoize it
let cts = new CancellationTokenSource()
memoizations.[key] <- cts
// Start a new debouncer
(async {
try
// Wait timeout to see if another event will cancel this one
do! Async.Sleep timeout
// If still not cancelled, then proceed to invoke the callback and discard the unused token
memoizations.Remove(key) |> ignore
fn value
with
| _ -> ()
})
|> (fun task -> Async.StartImmediate(task, cts.Token))
let debounce250<'T> = debounce<'T> 250 |
Beta Was this translation helpful? Give feedback.
-
Thanks. That method seems to work fine for a debounced data stream, but I'm having a hard time transferring that to e.g. cancellation of API requests, where the cancellation is not controlled from inside a data stream, but from outside - for example, by the user. In more concrete terms: The code above works because the debounce wrapper knows that there can be no simultaneous invocations of any single function/lambda through the debounce wrapper. Thus the debounce wrapper itself can cancel any previous invocation. Furthermore, this is the only time an invocation should be cancelled. This is AFAIK not generalizable to cases where an invocation (e.g. of a HTTP request) may be cancelled at any time by the user or other parts of the program. Please let me know if I'm still being unclear. (By the way, using |
Beta Was this translation helpful? Give feedback.
-
You are not still being unclear, @cmeeren. module Cmd =
// Found in Tim Lariviere's ElmishContacts/ElmishExtensions.fs file
let ofAsyncMsgOption (p: Async<'msg option>) : Cmd<'msg> =
[ fun dispatch -> async { let! msg = p in match msg with None -> () | Some msg -> dispatch msg } |> Async.StartImmediate ]
module App =
type Model =
{ .. }
let init () = { .. }
type Msg =
| Start
| Cancel
let mutable cts = new System.Threading.CancellationTokenSource()
// Any long-running work e.g. HTTP request
let work =
async {
while true do
printfn "Working..."
do! Async.Sleep 1000
}
let start = async {
cts.Cancel()
cts <- new System.Threading.CancellationTokenSource()
printfn "Start"
Async.StartImmediate(work, cts.Token)
return None
}
let cancel = async {
printfn "Cancel"
cts.Cancel()
return None
}
let update msg model =
match msg with
| Start -> model, Cmd.ofAsyncMsgOption start
| Cancel -> model, Cmd.ofAsyncMsgOption cancel
let view (model: Model) dispatch =
View.ContentPage(
content = View.StackLayout(padding = 20.0, verticalOptions = LayoutOptions.Center,
children = [
View.Button(text = "Start", command = (fun () -> dispatch Start), horizontalOptions = LayoutOptions.Center)
View.Button(text = "Cancel", command = (fun () -> dispatch Cancel), horizontalOptions = LayoutOptions.Center)
])) The entire file can be found in the following gist: FabTestCTS.fs Please let me know if that's an inadequate pattern or if you have suggestions for performance improvements. |
Beta Was this translation helpful? Give feedback.
-
@rastreus By the way,
@cmeeren Thanks for noticing it. I'll fix it! Regarding the initial question, I can't think of a good pattern to handle this gracefully. Personally, I would have gone the same way @rastreus did in his second example, with a mutable field. |
Beta Was this translation helpful? Give feedback.
-
Thanks. I have an app that is very API-based, where there is no technical reason why there can't be multiple requests in flight at the same time (even to the same endpoint). So I'd need a lot of mutable fields. Could probably encapsulate the "cancel and set a new I'll figure out something, though. When/if I ever get to rewriting the app using Fabulous. :) I.e. probably not before #383/#241. My question is answered, so feel free to close this whenever or keep open to encourage more discussion. |
Beta Was this translation helpful? Give feedback.
-
I'm not sure if it would apply to your situation, but I have had great success managing long running web operations with MailboxProcessors. That way you can have a replaceable cancellation token without a mutable field, you can post start / stop messages etc to manage the connection and you can easily spin up multiple instances to run requests in parallel. There are loads of ways to adapt it to the MVU loop, if you would like me to suggest something I would be happy to but don't want to spam the thread! |
Beta Was this translation helpful? Give feedback.
-
Since my original question is more or less answered (there does not currently exist a definitive recommended pattern for cancellation in an Elmish architecture), there isn't really much to derail here. So yes please, I'd appreciate any input. :) (I'll leave it to the maintainers to choose when to close the task, since they may or may not want to keep it open as a reminder to create some recommendations here.) |
Beta Was this translation helpful? Give feedback.
-
Ok cool :) I'll start with a little caveat that I haven't actually used Fabulous itself as we don't use Xamarin Forms at work, but I have recreated it for Xamarin Native by pinching heavily from here. I'll describe a simple one-endpoint-at-a-time version but you could expand this out to handle concurrent requests using some kind of dictionary or list – again, if you would like me to try to elaborate on that I would be happy to. So, to start with I would add an argument to your update function to pass in the API function you want to call. This API function would have a mailbox processor pre partially applied. That means of course that every time you provide the input argument, it will always get posted to the same mailbox. That mailbox processor in turn will need a reference to the main Program dispatch baked in so that it can post results back to the updater. The arguments to the API function can be along the lines of Run of Request * ResponseMsg or Cancel etc. The API function will simply post the message to the mailbox – this means you can start it with Cmd.ofFunc. Internally the mailbox would maintain a cancellation token, a function to execute the API request, and a match to handle the incoming messages. Every time the mailbox receives a message, it will cancel its existing token, aborting any running job, and create a new one. It will then switch on the incoming message – if Cancel, job done and return. If it is a Run request, it will bundle up the Program dispatch, the Request and the ResponseMsg and run them immediately, not awaiting the result, and move on to pull the next message from its inbox. When the response returns it will be dispatched back to the update function. One thing that isn’t so great here is that the update func needs to know its position in the hierarchy in order to construct the appropriate message to return – if it is a child of a child of an updater, it will need to wrap its return message appropriately so that it can make its way back from dispatch. In my example below I have assumed it is top level for simplicity. I guess alternatively you could pass in the pre-constructed return message as a parameter to the update function, that way it would be easy to refactor and keeps the hierarchy knowledge outside of update. If this needed to be parallel, the mailbox could hold a map of running operation IDs and cancel tokens or something, or perhaps delegate to child mailboxes. I'm sure you can probably work something out there, the update loop is the important thing, and the fact that you don’t need any mutable state. I would recommend Vaughn Vernon's Reactive Messaging Patterns with the Actor Model as a great reference to all the cool ways you can use actors, and I think they fit really well with MVU. (Ignore the Scala / Akka mention - that is just the system he uses to demonstrate but most of the book is just reference patterns which apply anywhere.) I haven’t run the example below of course, it is just psuedocode but it is pretty much valid F# and I have implemented very similar in my apps. type ApiRequest = ApiRequest
type ResponseMsg = Result<string,exn>
type UpdateMessage =
| Run of ApiRequest
| Cancel
| Running of unit
| Cancelled of unit
| ApiResponse of ResponseMsg
| ApiError of exn
type ApiMessage =
| Run of ApiRequest * (ResponseMsg -> UpdateMessage)
| Cancel
let apiEndpointManager
apiClient
dispatch =
MailboxProcessor<ApiMessage>.Start (fun inbox ->
let rec innerLoop (cts : CancellationTokenSource) =
async {
let execute request handler =
async {
let! response = apiClient request
dispatch (handler response)
}
let! message = inbox.Receive()
cts.Cancel()
let newCts = new CancellationTokenSource()
match message with
| ApiMessage.Run (request, handler) ->
let op = execute request handler
Async.StartImmediate(op, newCts.Token)
| ApiMessage.Cancel -> ()
do! innerLoop newCts
}
innerLoop (new CancellationTokenSource()))
let apiFunc
(apiManager : MailboxProcessor<ApiMessage>)
apiMessage =
apiManager.Post apiMessage
let update
(apiFunc : ApiMessage -> unit)
msg
model =
match msg with
| UpdateMessage.Run rq ->
let newCommand = Cmd.ofFunc apiFunc (Run (rq,ApiResponse)) Running ApiError
model, newCommand
| UpdateMessage.Cancel ->
let newCommand = Cmd.ofFunc apiFunc Cancel Cancelled ApiError
model, newCommand
| UpdateMessage.Running -> model, Cmd.none
| UpdateMessage.Cancelled -> model, Cmd.none
| UpdateMessage.ApiResponse response -> model, Cmd.none
| UpdateMessage.ApiError e -> model, Cmd.none |
Beta Was this translation helpful? Give feedback.
-
Thanks! If I understand correctly, then in general, this seems to be a significant complication of the simple Elmish architecture.
I find the simplicity of the Elmish architecture one of its most important benefits, so personally I probably won't explore your method further. I have no problems with "pragmatic impurity" (e.g. mutable fields) as long as it's contained/encapsulated and easily understandable. But thanks anyway for taking the time to do the write-up; it's good to have different methods in the discussion. :) |
Beta Was this translation helpful? Give feedback.
-
Well, I wouldn't say significant personally. I think it is using it how it is designed to be used, not complicating it. Including functions that you want to call as arguments to update is recommended practice. You are calling this function using Cmd, which is how Elmish should work. You partially apply these at composition time, so they are invisible in the main application. Using dispatch to send messages through the update hierarchy is also the way things are supposed to be done - it is the message bus for the application. That is how Cmd.ofSub etc work. You could make a new command to handle it or something if you use it a lot. As I suggested (in a later edit) you could also pass the return message constructor into the update function at composition time, then the knowledge about position in the hierarchy is pushed out to the edge of the application. The bottom line is, you need to maintain and manage some state, and IMHO the best way to do that in a functional language is usually with an actor (i.e. mailbox), not a mutable field. The example I gave also does not require the API functions to be in the same module as the update (to access the shared mutable field). I think it is a pretty simple and idiomatic solution but I respect that you might not see it that way. Cheers :) |
Beta Was this translation helpful? Give feedback.
-
This might help you a bit with the way you can bend Elmish to your needs (apologies if you know it all already, I have no idea what your experience is!) See the 'Everything is a function' section in particular : https://medium.com/@MangelMaxime/my-tips-for-working-with-elmish-ab8d193d52fd The key thing to note here is that where he is calling other funcs in the module directly using Cmd.whatever, I am essentially suggesting you do the same. I just pass them in to update as a parameter when composing the application rather than directly calling one func from another in the same module, as otherwise you end up with coupled code and it also makes update unit testable (parameterise all the things, as Scott Wlaschin says!) |
Beta Was this translation helpful? Give feedback.
-
I would do something simple. Maybe something like this: module Cmd =
let ofCancellableAsyncMsg (p: Async<'msg>) : (unit -> unit) * Cmd<'msg> =
let cts = new CancellationTokenSource()
(fun () ->
use source = cts
source.Cancel()),
[ fun dispatch -> Async.StartImmediate(async { let! msg = p in dispatch msg }, cts.Token) ] Then it would be called with let update msg model =
match msg with
| CaseThatCreatesSomeCancellable ->
let (cancel, cmd) = Cmd.ofCancellableAsyncMsg (*cancellable workflow here*)
{ model with ToCancel = cancel::model.ToCancel }, cmd
| CaseThatCancelRunningCancellables ->
for cancel in model.ToCancel do
cancel()
{ model with ToCancel = [] }, Cmd.none |
Beta Was this translation helpful? Give feedback.
-
That's cool but you are still left with a list of functions which have refs to the cancellation token in your model aren't you? |
Beta Was this translation helpful? Give feedback.
-
Yes, and also probably leak if |
Beta Was this translation helpful? Give feedback.
-
Yeah, it is simpler than what I suggested for sure, although not by much if you squint for a minute and definitely not if you have to start including more complex request management. All I can say is that using a mailbox to manage the cancellation state is robust enough to be used in a production application and is very flexible as the messages it receives form a nice API to manage the requests, and you can even use the queue to manage back pressure or prioritisation. How exactly you fit them into the loop is obviously very flexible - my solution was only a suggestion. |
Beta Was this translation helpful? Give feedback.
-
I was talking about Elm or Elmish really - as I understand it, Elmish is basically 'Elm for F#' but a separate project like Fabulous ? I mean surely if you are making an Elmish application, you need to deal with cancellation and state management right? Or maybe I am missing something. I don't want to get off topic, sorry! I also think that it is important to make the point that this seems to ultimately be a conversation around state management (external to the model, non-serialisable), although the OP was about cancellation, because that is an example of a state problem. There must be an established way of doing that in TEA apps? I would love to 'taste the pudding' (that should totally be Fabulous's tag line btw haha) but I am working at a .Net shop that is currently exclusively C# MVVM for native Xamarin - I am very close to winning them over to F# for the core, but they will be using the Android and iOS designers for the forseeable future. To be fair, I also have no interest in Xamarin Forms, it just isn't a sensible proposition for most of our apps which need fine grained platform specific UI for picky clients. I will look at Elmish for the website to back my current app though. MVU seems like a sensible way of dealing with the vm / core interaction in my apps using the half-elmish style (rather than the loose layered systems I had come up with before) as it is consistent, symmetrical, strongly structured but very flexible etc. The pattern is very easy to grasp, even for beginners, and I could recreate the whole system in a few lines of code with no external dependencies - a big win in my book and should help when onboarding new employees. It does mean that I am less worried about what has gone before so to speak (from a consistency perspective - obviously I don't want to reinvent the wheel or repeat mistakes), although I am very interested in all of these discussions as I learn a lot and would love to contribute to this project in the future. :) Just back on the topic again - perhaps all of this isn't actually relevant to TEA, as in, TEA doesn't tell you what functions to run, rather how to run them. Using the mailbox to manage a piece of state is choosing what to run, then using update and a Cmd to run it is the how. Just another potential perspective on things. Edit : Just reading the reddit threads you posted, really useful, thanks - I think you got me to them just quickly enough to save me a lot of wasted hassle from over-componentising! :) |
Beta Was this translation helpful? Give feedback.
-
Ok how about some potentially useful code for once instead of all my waffle:
How does this seem for a command that wraps up all the stuff I wrote originally, no need for applying the dispatch to the mailbox in advance, no need for the mailbox wrapper, no need for a component to know its position. Much more like standard TEA. let ofActorAsync
(actor : MailboxProcessor<'a * ('b -> unit) * AsyncReplyChannel<'c>>)
(arg: 'a)
(ofComplete: 'b -> 'msg)
(ofRunning: 'c -> 'msg)
(ofError: _ -> 'msg) : Cmd<'msg> =
let bind (dispatch : Dispatch<'msg>) =
async {
let! r = actor.PostAndAsyncReply (fun asyncReplyChannel -> arg, ofComplete >> dispatch, asyncReplyChannel) |> Async.Catch
dispatch
(match r with
| Choice1Of2 x -> ofRunning x
| Choice2Of2 x -> ofError x)
}
[bind >> Async.StartImmediate] I think this could be a nice way of managing non-model state with a little polish and thought maybe. It could probably be named more appropriately. All you need to do now is design a mailbox to handle the state transformation and execute the callback, which is no more effort than writing the function/s you would have done previously to manipulate the mutable field. Just think of it as a function with a state bucket in this context. If it would help, I could show how my original example dealing with cancellation would look under this system, but you can probably work it out. I could also potentially adjust this to return an IDisposable token in the Running message, which would allow cancellation of the callback if necessary whilst allowing the operation to complete, basically making it an Observable. |
Beta Was this translation helpful? Give feedback.
-
Great! :) You can also check out my comment to this video and the links in that comment.
Please do! Cancellation and non-model state management in Elm/TEAI investigated a bit, and it seems that for cancellation, Elm might use mutable state, but it's abstracted away from the user in the Http module. The relevant functions are: request :
{ method : String
, headers : List Header
, url : String
, body : Body
, expect : Expect msg
, timeout : Maybe Float
, tracker : Maybe String
}
-> Cmd msg cancel : String -> Cmd msg When you create a request, you can supply a string as the I imagine that the user-supplied For completeness, let me mention that the track : String -> (Progress -> msg) -> Sub msg where type Progress
= Sending
{ sent : Int
, size : Int
}
| Receiving
{ received : Int
, size : Maybe Int
} I like this design because it means that, at most, you only have to store strings in the model, and the user is always in control of how to use the API. Writing a similar library is probably not that hard. I always abstract away |
Beta Was this translation helpful? Give feedback.
-
Ah that is really interesting and may be useful to me as a pattern moving forward. I will also check out the video / comments :) What do you think of the cmd I posted above? I still think this is a better general purpose solution for when you need to maintain state outside of the model, not just a cancellation token. This is really common in mobile apps, perhaps less so on the web side of things. I often need disposable tokens for observables, cached data from the database, objects which are being processed for whatever reason etc. These need to be stored somewhere other than the model. They do not need to be persisted or displayed, they are just objects that are in use at runtime to make the app function. This is why I find the mailbox such a useful solution, and the cmd I suggested hides all of the implementation away in a nice Elm like manner. I have a bit of a deadline today but will put up an example of it used to do the cancellation later, that should illustrate it better. |
Beta Was this translation helpful? Give feedback.
-
Looking forward to your example! I'm not that used to actors/mailboxes, so I need to see an example usage to be able to comment on it. No hurry, take your time. I have found other solutions to the same problems. For example, a cache can essentially just be a configurable (and composable) function wrapper with enclosed mutable state (think of a basic memoization implementation). I manage caching (and other resilience patterns) using Polly, which I highly recommend (I wish it had a more F#-friendly API, though). For disposables and cancellation tokens, I have abstracted away mutable state in an ad-hoc manner similar to Elm's As I said, I'm not used to actors, but AFAIK an actor is an addressable process, often a state machine, that encapsulates mutable state in a concurrency-safe manner. They therefore seem more heavy-weight than simple, composable functions - and actors are not easily composable. In the language of Tomas Petricek, an actor seems to have some similarities with a framework, since it controls how your code is run rather than the opposite. Specifically regarding using actors for caching, I also came across the article Don't use Actors for concurrency, see the section "Caching is not state". |
Beta Was this translation helpful? Give feedback.
-
That was an interesting article. He makes some points which I am sure are valid on fully implemented actor based systems like Akka running at scale. I also totally agree that there is no point in using an actor if you don't need to manage state, just use a normal function. I think his definition of cache is a bit different to mine, I just mean some data needed at runtime but not stored on disk, or an in-memory reflection of some on-disk data to make access easier and faster, not a static store. I will start here by saying that I am a fairly junior programmer, about 5 years pro, and only a year on F#, and mostly self taught, so I am probably a little off the mark on some things here and welcome any input from more knowledgeable people. I think it is initially important to make a distinction between using actors and the actor model of software design. At a very basic level, an actor is just a recursive loop with a message queue that runs on its own thread - no more, no less. I really like the fact that F# has a simple, lightweight and stripped down implementation included out-of-the-box, the MailboxProcessor. As it just uses a delegate that runs recursively, you can replace the state on each loop, and because it has an inbox you can post it messages and it will work through them as and when it can. The actor model isn’t a framework, it is a configuration-based model of computation and interaction, as opposed to a traditional state based turing machine model. Frameworks like Akka build on the concepts by providing tools such as an actor registry / resolution system, distribution across machines, failure handling, loads of stuff, but they aren’t the model themselves any more than Fabulous ‘is’ MVU. If you are interested in the fundamental concepts and power of the model-proper, there is an essential talk by the guy who invented it, Carl Hewitt (interviewed by Eric Meijer) here: https://www.youtube.com/watch?v=7erJ1DV_Tlo There is also a great amount of info here: There is obviously a lot of potential to model complex applications using a full, strict system like this, and arguably you would get more out of it the more you embrace it, much like MVU and FP in general. However, most of this is way more than I need right now for a mobile app and certainly a whole world away from what I am suggesting in the context of this thread. I think talking about the actor model is a bit like event sourcing – everyone has a different understanding (or lack thereof) of exactly what it is or how you do it or to what extent. I have taken the same approach here as I did there – try to understand the fundamental concepts, then implement the bits I understand which are useful in a pragmatic way to improve my daily code, rather than try to go full bore into creating a pure event sourced or actor based application because it is the latest thing and making an expensive mistake, or alternatively ignore it altogether and keep on doing things the inefficient way I had always done them. A little background: I create Xamarin apps at work, previously in C# exclusively. I received the Reactive Patterns w/ Actors book I linked to earlier as a gift, and found it fascinating although a little heavyweight as Akka is a full framework and overkill for my needs and Scala makes my eyes bleed. Most of the book however is just a general pattern reference, it isn’t that specific to the language or framework. A little while later I found (fell in love with) F# and started using it for the core Xamarin work. I needed to manage some application state, and the mailbox processor was easy to use and worked really well for many things. It allowed me to make use of many of those patterns where appropriate whilst not marrying a large framework, everything I needed was right there in the language. It also was a revolution when dealing with the database. I no longer need to worry about concurrent writes. I have a single mailbox which manages the write connection. No more locks or anything. My reads happen using separate actors – I currently have one per table as I have a generic implementation that I instantiate with a return message type. This is my own ad-hoc approach which fits my apps, and is a lot better than what I had before (locking a SQLite connection). I never need to worry about massive parallel access, as my apps don’t do that. If they did, I would research an appropriate solution for that scenario. The one thing I did find on my last project was that without a formal way of connecting the mailboxes, it can be hard to follow the flow of control for anyone unfamiliar with the architecture in question. That is why I have embraced MVU – it gives a rigid backbone for the application which can call into side-processors as necessary, making it easy to navigate and reason about. It may not be perfect, and it may not be strict MVU or Actor Model or FP or OO or whatever, but I know why I made every decision (and compromise), it works really well and I have basically no external dependencies. It is the best of all the worlds I know of done the best way I know how, which is all any of us can do hey. I am working on a proper project with all of this included right now which I am happy to share if anyone is interested, it will end up on Github eventually. Ok, so all of that said, I will reiterate that I am just simply talking about using the mailbox processor as a state bucket in this context, the most basic way you could think of. No big computing model or framework to deal with, just a small recursive loop with a stashed thing in it. The most basic way you can maintain state without a mutable field, and arguably simpler to reason about. How would I implement the cancellation loop using the cmd I posted previously? Something like this: type ApiRequest = ApiRequest
type ApiMessage =
| Run of ApiRequest
| Cancel
type ApiCallback =
| Reponse of Result<string,exn>
| Cancelled
type UpdateMessage =
| RunRequest of ApiRequest
| CancelRequest
| Running of unit
| Cancelling of unit
| ApiComplete of ApiCallback
| ApiError of exn
type Dispatch<'a> = 'a -> unit
type ApiEndpointManager<'a> = MailboxProcessor<ApiMessage*Dispatch<'a>*AsyncReplyChannel<unit>>
let apiEndpointManager
apiClient = // partially applied during construction
ApiEndpointManager.Start (fun inbox ->
let rec innerLoop (cts : CancellationTokenSource) =
async {
// Run the request and callback when done
let execute request dispatch =
async {
let! response = apiClient request
dispatch response
}
//Grab a message + dispatch and replychannel from the inbox. Dispatch already has return message precomposed in Cmd.exec.
let! message, dispatch, replyChannel = inbox.Receive()
// Cancel existing token and create a new one
cts.Cancel()
let newCts = new CancellationTokenSource()
// Switch on message and act appropriately
match message with
| ApiMessage.Run request ->
let op = execute request dispatch
Async.StartImmediate(op, newCts.Token)
| ApiMessage.Cancel -> ()
replyChannel.Reply() // Use this to reply with Running content
// Call self with new state
do! innerLoop newCts
}
// kick off the recursive loop
innerLoop (new CancellationTokenSource()))
let update
(apiFunc : ApiEndpointManager<ApiCallback>)
msg
model =
match msg with
| UpdateMessage.RunRequest rq ->
let newCommand = Cmd.ofActorAsync apiFunc (Run rq) ApiComplete Running ApiError
model, newCommand
| UpdateMessage.CancelRequest ->
let newCommand = Cmd.ofActorAsync apiFunc Cancel ApiComplete Cancelling ApiError
model, newCommand
| UpdateMessage.Running () -> model, Cmd.none
| UpdateMessage.Cancelling () -> model, Cmd.none
| UpdateMessage.ApiComplete callback -> model, Cmd.none
| UpdateMessage.ApiError e -> model, Cmd.none
I have included the types for completeness, it should actually compile if you import the cmd from earlier in the thread. As previously mentioned, this example is a 'one request at a time' set-up where each cancels the last, but you could easily expand it, and adapt it to handle any kind of state. The important bit is how nice it looks in the update func now, and if you strip all the comments out of the mailbox it is only a few lines. You are completely in control of how you use it, that depends on the mailbox body, and the Cmd is very generic, just like OfAsync but with an extra callback message. |
Beta Was this translation helpful? Give feedback.
-
On a side note, Polly looks really cool and I have bookmarked it but again way way more than I need for simple line-of-business Xamarin apps in general. Here is my own observable caching implementation using a mailbox, complete code: type EventBus<'T> () =
let _event = new Event<_>()
let _onPublish = _event.Publish
[<CLIEvent>]
member this.OnPublish with get() = _onPublish
member this.Invoke(arg : 'T) = _event.Trigger(this, arg)
type Cache<'Tag,'Content> =
{
Content : 'Content
}
type CacheCommand<'T> =
| Update of 'T option
| Fetch
| Subscribe of (obj * 'T option -> unit)
type CacheResult<'T> =
| Updated
| Subscribed of IDisposable
| Cache of 'T option
type CacheOperation<'T> = CacheCommand<'T> * AsyncReplyChannel<CacheResult<'T>>
let cacheProcessor<'T> =
MailboxProcessor<CacheOperation<'T>>.Start (fun inbox ->
let rec innerLoop ((currentCache : 'T option),(eventBus : EventBus<'T option>)) =
async {
let! (command, replyChannel) = inbox.Receive()
let result, newCache =
match command with
| Update maybeItem ->
try
eventBus.Invoke(maybeItem)
with _ as ex -> () // Report to error tracking?
Updated, maybeItem
| Fetch ->
(Cache currentCache), currentCache
| Subscribe handler ->
let token = eventBus.OnPublish.Subscribe(handler)
try
handler(obj(), currentCache)
with _ as ex -> () // Report to error tracking?
(Subscribed token), currentCache
replyChannel.Reply(result)
do! innerLoop (newCache,eventBus)
}
innerLoop (None, new EventBus<'T option>()))
[<Preserve(AllMembers = true)>]
type ObservableCache<'T> () =
let mailbox = cacheProcessor<'T>
let postCacheCommand
(message : CacheCommand<'T>) =
mailbox.PostAndAsyncReply(fun asyncReplyChannel -> message, asyncReplyChannel)
interface IObservableCache<'T> with
member this.Post (command : CacheCommand<'T>) =
(postCacheCommand command) |> Async.StartAsTask
Which I register with Autofac like this, allowing me to create a unique cache channel on the fly by resolving it with a type. builder.RegisterGeneric(typedefof<ObservableCache<_>>).As(typedefof<IObservableCache<_>>).SingleInstance() |> ignore Again, no external dependencies (other than Autofac but we use that for the C# DI anyway), simple and does the job. For example, if you want to receive customers, inject a Customer cache and post Subscribe, keeping hold of the returned disposable token. If you want to send, inject the same cache type and post Update. That's it. |
Beta Was this translation helpful? Give feedback.
-
Again, I may be biased by my unfamiliarity with actors/ [<AutoOpen>]
module Domain =
type SignInRequest = { Username: string; Password: string }
type SignInResponse = { Token: string }
type SignInError = InvalidCredentials | Exn of exn
module Http =
module SignIn =
let mutable private cts = new CancellationTokenSource ()
let cancel () =
lock cts (fun () ->
cts.Cancel ()
cts <- new CancellationTokenSource ()
)
let private constructRequest
: SignInRequest -> HttpRequestMessage =
failwith "not implemented"
let private parseResponse
: HttpResponseMessage -> Async<Result<SignInResponse, SignInError>> =
failwith "not implemented"
let send (request: SignInRequest) : Async<Result<SignInResponse, SignInError>> =
async {
cancel ()
use client = new HttpClient ()
use req = constructRequest request
use! resp = client.SendAsync(req, cts.Token) |> Async.AwaitTask
return! parseResponse resp
}
type Model = { IsSigningIn: bool }
type Msg =
| SignInRequested of SignInRequest
| SignInCompleted of Result<SignInResponse, SignInError>
| SignInCancelRequested
| SignInCanceled
let update msg model =
match msg with
| SignInRequested req ->
let cmd = Cmd.OfAsync.perform Http.SignIn.send req SignInCompleted
{ model with IsSigningIn = true }, cmd
| SignInCompleted _ ->
{ model with IsSigningIn = false }, Cmd.none
| SignInCancelRequested ->
let cmd = Cmd.OfFunc.perform Http.SignIn.cancel () (fun () -> SignInCanceled)
model, cmd
| SignInCanceled ->
{ model with IsSigningIn = false }, Cmd.none Furthermore, it occurred to me that it feels a bit weird to start/run actors using As for caching: Yes, Polly supports a lot of use-cases, but basic usage is very simple: let private cachePolicy =
Policy.Cache(
MemoryCacheProvider(MemoryCache(MemoryCacheOptions())),
TimeSpan.FromMinutes 5.)
let private someFunctionUncached () =
...
let someFunction =
cachePolicy.Execute((fun _ -> someFunctionUncached ()), Context("cacheKey")); It also just as easily supports more advanced use-cases (async, cache management, callbacks on cache hit/miss, etc.). In general, regarding Polly: Resilience patterns are simple at the fundamental level, but real-world concerns and edge cases can quickly make any implementation fairly complex (I tried, failed, looked at the Polly code for inspiration and then decided to cut my losses). I'm more than happy to let (what I understand to be) the de facto standard .NET resilience library handle that for me. :) As a big plus, plugging in other supported policies like circuit breakers, timeouts, fallbacks etc. and composing them together is very simple. A final note on recursive async calls: Always use |
Beta Was this translation helpful? Give feedback.
-
Great tip on the return!, thanks. I guess you haven't watched the Carl Hewitt talk yet. There is a whole lot more you get from the simple abstraction, but you will need to let it sink in for a bit I expect, it took me a while. You have the state. What you are missing is the queue, and the thread. The fact that you get a proper model of an indeterminate system. I'm not sure how you would spin up multiple requests at once with your module approach - you would need more mutable values. You can't instantiate a new module, you can spin up a new actor. Also threading might be an issue I guess? You could work around all that of course but your solution would soon look a lot more complex than mine I would wager. I personally find the example I gave cleaner and easier to reason about, but I guess that is just personal preference. Also, I would say again that I was only giving a simple example of how you might incorporate actors - let your imagination run wild. Just think of it as a safe way to handle state. It is so lightweight. For instance, you mention Cmd.OfSub. Well, that is exactly how I use them in my app. You see my ObservableCache in the last post? I have a function that I call in update's init using Cmd.ofSub, which posts a subscribe message to a cache and returns the disposable to update. Any values emitted by the cache are then sent into my update function as messages. Again, this is like 15 lines of code. Both the cache and this func are generic so only have to be written once. let initCacheObserver
(cache : IObservableCache<'Tag*'Content>)
onCacheUpdated
onSubscribed
dispatch =
async {
let cacheUpdatedHandler (sender, (maybeCache : ('Tag*'Content) option)) =
maybeCache
|> Option.map (fun (tag,data) -> data)
|> onCacheUpdated
|> dispatch
match! cache.Post(CacheCommand.Subscribe cacheUpdatedHandler) |> Async.AwaitTask with
| CacheResult.Subscribed token -> dispatch (onSubscribed token)
| _ -> ()
} |> Async.Start
... match message with
| SurveyListMsg.Init ->
let subToSurveyCellsCmd = Cmd.ofSub (initSurveyCellModelCacheObserver SurveysUpdated SubscribedToSurveys)
return model, subToSurveyCellsCmd, surveyUpdatesToken
| SurveyListMsg.SubscribedToSurveys token -> return model, Cmd.none, Some token
| SurveyListMsg.SurveysUpdated maybeSurveys ->
let surveys = maybeSurveys |> Option.defaultValue Seq.empty
let newModel = {model with Surveys = surveys; Loading = false}
return newModel, Cmd.none, surveyUpdatesToken |
Beta Was this translation helpful? Give feedback.
-
@RyanBuzzInteractive I am thoroughly interested in the actor model and would love to check out the project that you're working on. The project that I am working on at my day job will soon be refactored to a service-oriented architecture using the actor model (through Akka.Net) so I've been studying a lot lately. I'm also the sole Xamarin developer so incorporating actors into my mobile app is interesting. |
Beta Was this translation helpful? Give feedback.
-
I have read a bit about actors in the last few months and even went to an actor workshop on a conference recently. I'm certainly a beginner when it comes to actors, but I can appreciate that a proper model of a clearly encapsulated, indeterminate system is a useful abstraction in many contexts - for service-oriented architectures, to ensure concurrent-safe access to mutable state, to create immutable state machines (which AFAIK is the core of MVU - in fact, Elmish used MailboxProcessor previously), etc. In this particular case, my desired functionality is a Cmd that calls an API and responds with a relevant message, and I want the API request to be cancelable. As my example shows, this can easily be done with simple functions. I don't see what I gain by having "a proper model of an indeterminate system"; it seems like an unnecessary complication in this case. The same goes for the caching. I get why actors are great in many other contexts; I just don't see – again, in these particular cases – what they add over simple functions, that is worth the tradeoff. I am sure you agree that both in itself and because of the additional message types needed, an actor, modeling an indeterminate system, is a more complex abstraction than a simple function. Actors share some similarities with microservices communicating over some message bus: Both are isolated "processes" that only communicate asynchronously via messages. In light of this, why should I pay the microservice premium when the use-case is so simple that a "monolithic" architecture (normal, synchronous (whether (Oh, and a concrete problem I am struggling with at the moment: Stack traces may end up being much less useful, and debugging is much harder, when using completely asynchronous stuff like MailboxProcessors.)
You said your example was single-request only, so that's what I modeled, too. I was unsure if your example was intended to handle generic requests or a specific request, but I guessed the latter and therefore decided to call it something concrete, i.e. SignIn. If I needed to support multiple parallel, cancelable requests (for other use-cases than sign-in), then I might create a solution similar to Elm as previously described, where I pass a tracker string that I can later use to cancel the request. The solution would only be slightly more complex:
I don't see any immediate threading problems in my example, and thus no need for additional complexity. Let me know if I've overlooked anything. |
Beta Was this translation helpful? Give feedback.
-
Ok, I think we have probably reached our conclusion here, and I have really enjoyed the chat but we don't want to bore people going around in circles! Extremely interesting that Elm used a mailbox processor originally rather than the ring bus - my own MVU thing I am using is all using the Mailbox, so I am interested to know why they switched. What you have described as the solution to a threaded and concurrent requesting problem is exactly what I thought - IMHO a far more complex setup with a mutable dictionary, or that string based approach which is very limited to this particular application, not state management in general. That particular mailbox only dealt with one request at a time, but you could make many of them. They can hold any kind of multidimensional complex state you need, such as the event bus in my cache. At the end of the day, if you want to use mutable state safely, you need to encapsulate it. If you want to use concurrency you need to duplicate it. If you want to have a single value accessed by multiple threads you would need a queue. And hey presto, you just re-invented the mailbox. You can't avoid it, you can only reinvent it in other ways. If you are interested, have a peek at the F# guidelines here concerning mutability - pretty strict about how it should be used and contained (i.e. used for performance and very locally contained) : https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions I'm not encouraging you go crazy and re-architect your application exclusively around mailboxes. I am suggesting you use a built in language construct (they put it in the bcl for a reason) for the job it was intended to do, manage state simply and safely, and I am showing just a few basic ways that it fits neatly into the MVU pattern. If you prefer the approach you have suggested, that's totally fair - I just think you are painting yourself into a sticky corner you don't need to be in, and I think that by using the mailbox you would make your life simpler, by I respect you don't currently share that opinion. Thanks again for the conversation, and I have certainly learnt a few important things along the way! @rastreus Awesome! To be honest you probably know more about it than me if you have been properly studying it. Would be well up for a chat and sharing code. Perhaps we should catch up on Gitter or something to avoid spamming this thread? Drop me an email if you fancy a chat and I will log in :) |
Beta Was this translation helpful? Give feedback.
-
Elmish, not Elm. I have no idea what Elm uses. (Not MailboxProcessor of course, since that's F# specific). You can read about the reasons for the switch in this PR and the three linked issues: elmish/elmish#160
5 more simple-to-understand lines added to the already simple function-based solution is IMHO not vastly more complex - I'd say it's still very simple. Also, I'm trying to solve this particular case, not state management in general. As I said, I appreciate that actors/queues/etc. is a very powerful pattern and the right tool for the job in many contexts. :) Thanks for the conversation! I'll make sure to investigate MailboxProcessor a bit more on my own time. |
Beta Was this translation helpful? Give feedback.
-
Elm / Elmish - sorry, early morning typo! Seems like they dropped it because of React and Fable due to a fresh dispatch ref on each loop or something, but it was fine on .NET. At least it wasn't a performance thing, that is what I was concerened about down the line. Cheers dude! |
Beta Was this translation helpful? Give feedback.
-
Just a quick addendum for anyone who gets this far down - I was reading F# Deep Dives which @tpetricek put together with loads of other people - there is a fantastic chapter (8) called 'Asyncronous and Agent Based Programming' which is a great expansion on all the stuff I was on about. The whole book is golden, highly recommended :) |
Beta Was this translation helpful? Give feedback.
-
When calling a HTTP API or doing other long-running tasks, one might need to support cancellation. This could be explicit using a "Cancel" button, or implicit, e.g. cancelling any ongoing requests when signing out of the app.
Is there a recommended pattern for cancellation in Fabulous? Should
CancellationTokenSource
s for any unique cancellable request be stored in the model and replaced when cancelled, or are there better ways? (And would that cause issues if serializing/deserializing the whole model, includingCancellationTokenSource
s?)Beta Was this translation helpful? Give feedback.
All reactions