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

[Discussion] Should Cmd's be handled as "managed effects" like elm does? #123

Closed
nilshelmig opened this issue Nov 3, 2017 · 24 comments
Closed

Comments

@nilshelmig
Copy link

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>) like

  • query optimizations, for example - remove duplicated HTTP requests
  • testing the update function

Why hasn't elmish implement commands as "managed effetcs"?

@Zaid-Ajaj
Copy link
Member

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 Cmds parameterizable through the Program API; For example Cmd.ofPromise uses that Promise API directly but this could parametrized when the Program is Bootstrapped. In the unit tests, we would bootstrap the Program in a different way passing in our mock Promise implementations etc. Another priority is not to make the bootstrapping complex (definitely not how angular does things)

There were just my thoughts on the subject, I haven't tried anything concrete with code on the subject. More to come 😄

@et1975
Copy link
Member

et1975 commented Nov 3, 2017

I see managed effects as something Cmd could leverage, but not strictly necessary.

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 Platform to implement managed effects and still not have the safety guarantees of Elm because F# lets one mutate the state and access everything directly.

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:

  • testability of the update function: unless one messes with the world directly instead of using the passed in state and Cmd to issue effects the update function is still testable. It's probably not as nice as in Elm, but it's possible, for example: call the update directly, pass everything in, discard the Cmd. Yes, you can't write assertions against the effects, but imho that's not all that interesting.

  • debouncing of requests: this question came up just the other day (Q: Canceling of open promise commands #122) and because we have direct access to the platform and the mutable constructs of F# it can be implemented fairly easy.
    If someone has a nice generic implementation of this I'd gladly take that into Elmish core.

EDIT:
I'm talking about unit-testing, what @Zaid-Ajaj is suggesting is probably more of a sandboxing/mocking of the environment. Personally, I value unit-testability (even if I don't write actual tests) and leave everything else to a "real" setup and a human operator. I'm curious, which one are other people more interested in?

@nilshelmig
Copy link
Author

@Zaid-Ajaj
I had the same idea in mind. I would like to have the ability to register "effect managers" like this:

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!

@2sComplement
Copy link
Contributor

@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.

@nilshelmig
Copy link
Author

@2sComplement
It's a question about the perspective. If you take a look at the result, then there is no difference, because in both implementations the promise would be handled and you will get back the result via messages.

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.
This will prevent writing more than one implementation of how to do something.
For example, when I have a program with multiple update functions and some update functions can give back a command with its own implementation of how to handle a Promise, because the command is just a Dispatch<msg> -> unit .
Now, if someone reports that the handling of a promise is broken then I would need to look for the bug in every implementation.
If the command is just a description of what to do then I only need to look at the implementation of the "effect manager" and can trace the bug. And because the command is just a description I can debug the program by just take a look at the Msg and Cmd which enter the runtime and see where the problem occurs.

That the Cmd holds just information allows you to simply mock the "effect manager" in the Platform which is a big benefit for testability and prototyping!

@MangelMaxime
Copy link
Member

@nilshelmig Just to understand something before doing a more complete answer.

What you would like is something like: Http.send isn't it ?

@rommsen
Copy link

rommsen commented Nov 9, 2017

@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 😄)

@MangelMaxime
Copy link
Member

@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
Please do not use this code as it is in your application. It's can break your application and I just tested a mini prototype with it

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 setTimeout so the code execution is delayed. If we provide an API to allow this kind of Cmd it would be to the custom command here exec to handle all the possible side effects.

@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: Result<'success,'error> would allow all case.

We could have something like Result<'T,System.Exception> to have an equivalence to what we have today in Elmish. And allow custom Cmd implementation resulting into Result<'success, 'error>. For example: Result<'T,Http.Error>.


I am not sure if I am all clear, I rewrote this message a lot of time. Let's consider it good enough :)

@MangelMaxime
Copy link
Member

This discussion make me ask in fact, do we have a way to create now type of Cmd.<_> for Elmish. Because, here this is more or less what I did in my previous messsage.

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.

@2sComplement
Copy link
Contributor

It took a bit of reading on way Elm is actually implemented, but think/hope I understand: You want a Cmd that represents an effect (something like an HTTP call), and the implementation of this Cmd should be injectable so that it can be mocked out for a test.

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 Cmds, in effect creating new types of Cmd as @MangelMaxime alluded to.
We inject the interface into our update function, using IHttpCmd.Get just like any other Cmd:

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 Cmd.Promise is the thing you would want to mock out, since that doesn't really describe how something is done, but rather the nature of how it's done. Does this make sense or am I missing something?

@et1975
Copy link
Member

et1975 commented Nov 10, 2017

Thanks everyone for chipping in, I'm going to close this with following observations:

  • There's a clear delineation between Task and Cmd, the tasks come from the platform, Cmd - from Elmish. If you understand the relationship between the two then we have some common grounds for the discussion.

  • The code is data. I've had another look at Elm's Task in case I missed anything and it gives you nothing extra in terms of meta model or optimization opportunities compared to Elmish. Some comments alluded to the "interpreter" approach to dependency injection, but Elm does not do it. If that's of interest, I can provide some guidance as I've written an interpreter or two myself.

  • Whatever optimization you get, you get out of your "task" implementation, Cmd is just a carrier, it does what it's supposed to do and it's exactly as powerful as Elm's. The existing support for tasks is predicated on the available types of tasks, if you implement an interpreter-based tasks, just incorporate a Cmd factory for it and it will work with Elmish.

  • Cmd type signature itself does not prescribe how to map input/output, so if you don't like how ofPromise is implemented you can write your own. Keep in mind that the goal is to feed the results back into your update function. If you like dealing with Result<_,_> instead of plain old Msg cases - you can.

  • update in Elmish is unit-testable out of the box, and as @2sComplement demonstrates, not only you can simply discard a Cmd, but you can inject an implementation at runtime that you like. Sometime I need to parametrize my API functions, for example with a login token - in that case parametrized function (or the lack of) simply becomes part of the state - it's behavior is modified at runtime.

@et1975 et1975 closed this as completed Nov 10, 2017
@Zaid-Ajaj
Copy link
Member

The demonstration provided by @2sComplement is just perfect, I realized that parameterization of the the commands at the function level (of the update function) through normal parameters is much better than some magical and implicit "dependency injecters" at the API level of the Program module. Using either some interface or a single function as a dependency is a matter of personal preference.

To @nilshelmig, I think this really solves the testability question of the update function, turns out to be just plain old dependency management in the functional style.

@vbfox
Copy link

vbfox commented Jan 26, 2018

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)

@et1975
Copy link
Member

et1975 commented Jan 26, 2018

something that call a server

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 init, only the core's dispatch loop does the actual command evaluation, so what exactly is the problem?

@vbfox
Copy link

vbfox commented Jan 26, 2018

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.

@irium
Copy link

irium commented Jan 31, 2018

Completely agree with @vbfox : commands, returned from update should be testable.
I want to know not only the state returned, but commands also. They are equally important as the state.
And it could be nice to have this ability out of the box.

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()

Http.perform is defined like this:

[<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 :(
But at least it works with simple scenario like Increment / Decrement commands.

The real work is in let extractMsgs (cmd:Cmd<'a>) : 'a[] - it tries to extract 'Msgs from the Cmd.
This is the real difficult thing.

@kspeakman
Copy link
Contributor

kspeakman commented Nov 27, 2018

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, but Cmd still limits the ability to define them in a declarative/value-based way (not true, see below). What is needed is the ability to define our own effects as a normal DU with value equality instead of using Cmd. Then we would also need to define a new function to map the declared effect to an implementation. That would make the init/update effects testable. Update: see below for an example.

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.

@et1975
Copy link
Member

et1975 commented Nov 27, 2018

Again, I think the signature of the Cmd is exactly what it needs to be for what it's for, but I look forward to seeing all of your implementations. Btw, /elmish/oidc does Cmd injection, without any gimmicks.

@kspeakman
Copy link
Contributor

kspeakman commented Nov 27, 2018

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

@kspeakman
Copy link
Contributor

kspeakman commented Nov 28, 2018

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 init or update functions and assert what the output model and effects should be.

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

@OnurGumus
Copy link

@kspeakman I feel like things would go pretty wild once you have sub components with above approach

@kspeakman
Copy link
Contributor

kspeakman commented Oct 31, 2019

@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. 😄🤗

@OnurGumus
Copy link

@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.

@kspeakman
Copy link
Contributor

kspeakman commented Oct 31, 2019

@OnurGumus Wouldn’t scale in what way? Don’t tell that to our decent-sized code base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants