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
[Discussion] Should Cmd's be handled as "managed effects" like elm does? #123
Comments
Hi @nilshelmig I was thinking a lot along these lines, especially the ability to unit-test the update function which is super important. Right now, the "runtime" is a simple implementation of elm's architecture using an agent (mailbox processor). I am not sure about query optimizations but making the update function testable is a high-priority IMO. First thought that came to me is making There were just my thoughts on the subject, I haven't tried anything concrete with code on the subject. More to come 😄 |
I see managed effects as something Elm, being immutable, has to have managed effects (or it wouldn't have any effects). On the other hand, In Fable we can cause effects at will, but we'd have to reinvent the entire I think the way elmish is implemented is more inline with Fable's philosophy of leveraging what's out there instead of reinventing, but if someone wants to implement managed effects it would be easy to make use of it. With that in mind, specific concerns you mention I do want to address:
EDIT: |
@Zaid-Ajaj type PromiseEffects =
| Attempt of Promise * SuccesMsg // Start a promise
| ...
let promiseEffectManager (effect : PromiseEffect) dispatch =
match effect with
| Attempt (promise, successMsg) ->
// start promise and get result
result |> successMsg |> dispatch
Program.mkProgram init update (fun model _ -> printf "%A\n" model)
|> Program.registerEffectManager promiseEffectManager
|> Program.run I don't know how to implement this but at my work it would block everybody from writing own implementations and doing side effects. The type system would lead everybody to go the safe way! |
@nilshelmig Just trying to understand the concept discussed here. How is what you posted different from the current implementation: let update msg model =
match msg with
| Attempt -> Cmd.ofPromise (...) SuccessMsg FailureMsg
| SuccessMsg result -> // it worked
| FailureMsg e -> // something bad happened Maybe I'm missing the difference between a "managed effect" and "side effect" as shown above. |
@2sComplement From the technical side the difference is that Cmd.ofPromise gives you back a function which is evaluated at a later time while with "managed effects" the Cmd.ofPromise would give you back data which tells the effect manager what and not how you want to do. The idea behind this is that you have an architecture in which you describe what you want to do and all the implementation (the how) are hidden in the runtime. That the |
@nilshelmig Just to understand something before doing a more complete answer. What you would like is something like: Http.send isn't it ? |
@MangelMaxime : short answer is yes (answering for @nilshelmig here, but we are sharing a desk most of the time anyway, so I guess its ok 😄) |
@rommsen Yes I think this is ok :D So I am like @2sComplement I don't fully understand the "managed effect" VS "side effect". I think both have value and also both are doing in elmish. Disclaimer My idea, is Elmish could offer both way of working with "effect". This prototype is working: type Msg =
| Test
| TestSuccess of Result<string, exn>
| Test ->
let exec args toMsg dispatch =
Fable.Import.Browser.window.setTimeout(fun _ ->
match args with
| "generate a success" -> Ok "success"
| "generate an error" -> Error(Exception("error generated"))
| _ -> Error(Exception("unkwown case"))
|> toMsg |> dispatch
, 500) |> ignore
model, [ exec "generate a success" TestSuccess; exec "generate an error" TestSuccess ]
| TestSuccess result ->
match result with
| Ok success ->
Fable.Import.Browser.console.log success
| Error exn ->
Fable.Import.Browser.console.log exn
model, Cmd.none To demonstrate something equivalent to a network request, I used @et1975 The code I used here is something close to what we used for Fable-Arch. To be fair, my "doubt" with the current implementation of Elmish handling of errors is that it's using Exception which seems to not have all the info I would like or be flexible enough. Perhaps, if we would use something like: We could have something like I am not sure if I am all clear, I rewrote this message a lot of time. Let's consider it good enough :) |
This discussion make me ask in fact, do we have a way to create now type of And do we want it ? I don't see any reason to not want it as it can provide new helpers etc. After, people creating new commands need to make sure they are handling all case success and failure (exception) etc. |
It took a bit of reading on way Elm is actually implemented, but think/hope I understand: You want a I'll try to illustrate how I would see this working using a small program that calls a simple HTTP API. We can define an interface that represents a group of HTTP commands (only showing a GET here): [<Literal>]
let IpUrl = "https://api.ipify.org?format=json"
module Types =
type IpAddress = { ip: string }
type Model = string
type Msg =
| GetIp
| GotIp of IpAddress
| Error of string
type IHttpCmd =
[<PassGenericsAttribute>]
abstract member get<'a> : string -> ('a -> Msg) -> (System.Exception -> Msg) -> Cmd<Msg> We can provide real and mock implementations: module Cmd =
open Types
type Http() =
interface IHttpCmd with
[<PassGenericsAttribute>]
member __.get<'a> url success error =
Cmd.ofPromise (fun url -> fetchAs<'a> url []) url success error
type MockHttp() =
let getResp (url:string) =
match url with
| IpUrl -> { ip = "1.2.3.4" } |> toJson
| _ -> failwith "Unmapped url"
interface IHttpCmd with
[<PassGenericsAttribute>]
member __.get<'a> url success error =
Cmd.ofFunc (fun _ -> getResp url |> ofJson<'a>) () success error
let http = Http()
let mockHttp = MockHttp() Notice that both implementations are wrapping let update (http:IHttpCmd) (msg:Msg) (model:Model) =
match msg with
| GetIp -> model, http.get<IpAddress> IpUrl GotIp (string >> Error)
| GotIp ipAddr -> ipAddr.ip, Cmd.none
| Error e -> e, Cmd.none And then define the (real or mock) implementation when initializing the program: Program.mkProgram init (update Cmd.http) view
... This lets you parameterize those effects that you want for testing purposes. I'm not sure |
Thanks everyone for chipping in, I'm going to close this with following observations:
|
The demonstration provided by @2sComplement is just perfect, I realized that parameterization of the the commands at the function level (of the To @nilshelmig, I think this really solves the testability question of the |
Continuing from #130 (comment) The essence is that if you take the sample here https://fable-elmish.github.io/elmish/basics.html open Elmish
let init () =
{ x = 0 }, Cmd.ofMsg Increment I can't test the result of init without applying the command and if it is something that call a server I can't do that in unit tests so that's untestable. I can rewrite the code to be testable but I wonder if there isn't a way to make it a clear pattern in the types. (It's essentially the same problem as with Microsoft and the core of the framework. I can create interfaces for every sub system to make it testable but if it was provided out of the box there wouldn't be 4 or 5 projects on github with the single purpose of wrapping the file system API with interfaces) (And as said over there I have no idea if the additional complexity is worth it) |
I think you have a specific assertion in mind, but I'm not sure what it is. Your call to the server would not happen as result of calling |
Well init (or update it's the same) ended up after being called (or receiving a message for update) by returning N commands What are theses commands ? Do they match what I expect ? Except for their number I can't assert anything directly. let update msg model =
match msg with
| ChangeNuclearReactorParameters p ->
if complexCondition p (model.temperatureKelvin) then
Cmd.post "/safely_stop_reactor
else
Cmd.post "/explode_reactor I'm interested in testing this function (let's say that complexCondition expect a temperature in degrees) i'm interested in asserting what's happening. Obviously I can rewrite it (and all my app) to use dependency injection but the currently available commands in the core or elsewhere aren't designed for that so i'll wrap the world. And even worse if I use external elmish components (From other teams or from muget). all will use the standard commands, and I can hardly rewrite all of them. I can force my team to write everything with dependency injection in mind but I would prefer if the default path was easier. |
Completely agree with @vbfox : commands, returned from But, indeed, I have no idea how to implement it :( Currently, I'm using ugly dirty hacks like this: module Hook =
open Fable.Core.JsInterop
open Prelude.JsInterop
[<Emit("Http.perform")>]
let private httpPerform : obj = jsNative
[<Emit("Http.perform = $0")>]
let private hookHttpPerform obj : unit = jsNative
let withHttpPerformHook f =
let hook a b c =
let bind f = f (a, b, c)
[bind]
let prev = httpPerform
hookHttpPerform hook
try
f()
finally
hookHttpPerform prev
type HookBuilder() =
member __.Zero() = ()
member __.Delay f = f
member __.Run f = withHttpPerformHook f
member __.Combine(f1, f2) = f2(); f1
open Fable.Core.JsInterop
open Prelude.JsInterop
let extractMsgs (cmd:Cmd<'a>) : 'a[] =
let mutable msgs = [||]
let append x = msgs <- Array.append msgs [|x|]
let getMsg cmd' = cmd' append
cmd |> List.iter getMsg
msgs
let equalEffect (expected:Cmd<'a>) (actual:Cmd<'a>) =
let expected', actual' = extractMsgs expected, extractMsgs actual
actual' |> equalWithDiff expected'
/// Asserts effects equals.
let inline (=@=) a e = equalEffect e a
let assertState f state : unit = state |> fst |> f
let assertCmd f state : unit = state |> snd |> f
let testHttp = Hook.HookBuilder()
[<Pojo>]
type AsyncAction<'Request, 'Response> =
| Req of 'Request
| Rcv of 'Response
| Err of string
module AsyncAction =
let perform action factory args =
Cmd.ofPromise factory args (action << Rcv) (action << Err << string)
module Http =
let perform (action:AsyncAction<_,'b>->'c) (factory:_->JS.Promise<'b>) args = AsyncAction.perform action factory args And despite above, this doesn't actually work 100% as I want :( The real work is in |
Elm has limited effects it knows how to perform. So it takes total control in defining and performing them, with any "custom" effect being via ports. With Fable we have the freedom to perform our own effects, Personally, I have not unit tested front-end MVU applications. We have a largish one in production for a couple of years now and have never needed to. (Not that there haven't been bugs, but just not a big deal.) But I adapted the MVU style to run processes on the server-side. There I definitely want to test that the right effects are triggered. And the above is the conclusion/solution I arrived at. |
Again, I think the signature of the |
Yeah, you are 100% right @et1975. My apologies. I did not look closely enough at Elmish's Cmd which is just a list of functions that take a dispatch fn. (I'm used to Elm, so I had a different mental model.) It is imminently possible to use my own declared effects and effect function and just map them to Cmds when the Program is wired up. You can test your effects already. Example! type MyEffect =
| DoSomething
let perform effect dispatch =
match effect with
| DoSomething ->
...
dispatch (DidSomething (Ok ()))
// testable model and effects
let update model msg =
match msg with
| ... -> model, [ DoSomething ]
let updateAdapter model msg =
let (model, fx) = update model msg
(model, fx |> List.map perform)
Program.mkProgram ... updateAdapter ... |
Per request here is a complete runnable example of testable effects that I devved in Fable REPL. You can paste this code there to see it run. Open the dev tools and see what happens in session storage as you play with it. The actual tests are left as exercise to the reader in your favorite testing lib. (MSTest, XUnit, NUnit, whatever.) All you do is call the module Elmish.EffectsDemo
open Fable.Core.JsInterop
open Fable.Import.Browser
open Fable.Helpers.React
open Fable.Helpers.React.Props
open Elmish
open Elmish.React
// MODEL
type Model = {
Value : string
Info : string option
}
type Msg =
| ChangeValue of string
| Save
| Saved of Result<unit, exn>
| Load
| Loaded of string option
type Effect =
| Store of key:string * value:string
| Fetch of key:string
let perform effect dispatch =
match effect with
| Store (key, value) ->
try
sessionStorage.setItem(key, value)
dispatch (Saved (Ok ()))
with ex ->
dispatch (Saved (Error ex))
| Fetch key ->
let value =
match sessionStorage.getItem(key) with
| null -> None
| x -> Some (x :?> string)
dispatch (Loaded value)
let key = "fx_demo"
let init () : Model * Effect list =
{ Value = ""; Info = None }, [Fetch key]
// UPDATE
let update msg model : Model * Effect list =
match msg with
| ChangeValue newValue ->
{ model with Value = newValue }, []
| Save ->
{ model with Info = None }, [Store (key, model.Value)]
| Saved (Error ex) ->
{ model with Info = Some (sprintf "Save failed. Perhaps storage is full? %s" ex.Message)}, []
| Saved (Ok ()) ->
{ model with Info = Some "Saved successfully." }, []
| Load ->
{ model with Info = None}, [Fetch key]
| Loaded None ->
{ model with Info = Some "Nothing was stored." }, []
| Loaded (Some s) ->
{ Value = s; Info = Some "Loaded value." }, []
// VIEW
let view model dispatch =
div [] [
div [] [ str "Enter a value to save."]
input [
Class "input"
Value model.Value
OnChange (fun ev -> ev.target?value |> string |> ChangeValue |> dispatch)
]
span [] [str " "]
button [ OnClick (fun _ -> dispatch Save)] [ str "Save" ]
br []
div [] [str "-- OR --"]
br []
button [ OnClick (fun _ -> dispatch Load)] [ str "Load" ]
br []
br []
( match model.Info with
| None -> str ""
| Some s -> h4 [] [str s]
)
]
// helper
let mkProgramWithEffects init update view perform =
let initf arg =
let (model, fx) = init arg
(model, fx |> List.map perform)
let updatef msg model =
let (model, fx) = update msg model
(model, fx |> List.map perform)
Program.mkProgram initf updatef view
// App
mkProgramWithEffects init update view perform
|> Program.withConsoleTrace
|> Program.withReact "elmish-app"
|> Program.run |
@kspeakman I feel like things would go pretty wild once you have sub components with above approach |
@OnurGumus sorry I misunderstood sub to mean subscription in my previous, now deleted, response. It really depends on what you mean by sub-components. I do organize different pages into different modules. I do not pursue a component based model like what is typical in React. That is frequently premature optimization for reuse which may never occur. There are a few places where I abstract some UI pieces into reusable types and functions. E.g. paging controls. I don’t think it ends up too wild. 😄🤗 |
@kspeakman, yes I was referring to sub modules as sub components as in each of which having their own I it update and view. Your approach wouldn't scale when you have these sub modules. |
@OnurGumus Wouldn’t scale in what way? Don’t tell that to our decent-sized code base. |
In elm a command is a "managed effect" which means that a command contains information for the runtime to perform the side effect.
This gives some benefits over a lazy function (the currently implemented
Sub<'msg>
) likeWhy hasn't elmish implement commands as "managed effetcs"?
The text was updated successfully, but these errors were encountered: