marcosh / marcosh.github.io

4 stars 1 forks source link

Effects not considered #1

Closed kspeakman closed 8 years ago

kspeakman commented 8 years ago

Hi there!

Not sure if this is the right forum, but I don't use Disqus.

Anyway, one key thing missing from your event sourcing example is isolating effects. You can't realistically keep all messages in history, because some of them are non-deterministic; specifically the ones that produce "Cmd"s. Example: CreateCustomer might produce a Cmd (HTTP post) that succeeds the first time, but the second time generates an error. (You probably want it to generate an error and not duplicate the customer.)

This is the difference between a command and an event. Unfortunately, Elm does not make this distinction. Often a command doubles as an event because it needs to change the UI model. For instance, to turn on a spinner icon indicating your request is being processed.

I'm not sure if Elm allows it (still learning it), but theoretically you could fix this by not updating the model on the initial command message, then issuing two event messages from the Cmd. Example: user click triggers CreateCustomer command message. Handling that produces 2 "Cmd"s. The first statically returns CreateCustomerRequested event message. The second does the HTTP side effect and returns its result as another event.

So if we only have events in history, then the state is also replayable in isolation of outside factors. I may post something on elm-discuss about this as well. I think Elm's current time-traveling has to just ignore commands in order to achieve this.

And thanks for your article! It opened my eyes to what exactly was gnawing at me about returning (model, Cmd.none) on updates.

marcosh commented 8 years ago

Hi @kspeakman, thanks for your comment!

I definitely agree with you regarding the fact that Effects are not considered in the simplified approach I proposed. I intentionally avoided effects in my example to keep things simple, and not incur in the problems you mention. What I think could be the solution is using CQRS, explicitely separating Commands from Events, and I'm planning another blog post on this.

Regarding the time travel debugger, I'm not an expert on the subject, but here it states

Because Elm represents side-effects explicitly as values, the debugger just needs to tell the runtime not to perform any side-effects during replay to avoid these issues

kspeakman commented 8 years ago

I haven't looked at the code, but it sounds like the TTD has special runtime hooks. That probably wouldn't work for event-sourcing.

I do have a slightly more fleshed out example on elm-discuss that could possibly be used now. As you say, it takes the idea from CQRS. But the essential goal is just isolating event messages (facts) which cause model updates.

elm-discuss thread

I would love to see another article about it if you find the time. :)

kspeakman commented 8 years ago

I experimented with adding full blown Command/Event pattern into the mix. Here is the result. (This works in Try Elm.)

import Html exposing (Html, div, button, text)
import Html.App as App
import Html.Events exposing (onClick)
import Task exposing (Task)

main =
  App.program
    { init = init [ Incremented 1, Incremented 2 ] -- need a way to pass facts in
    , view = viewAct >> App.map Perform
    , update = update
    , subscriptions = \ _ -> Sub.none
    }

type alias Model =
  { history : List Fact
  , view : Int
  }

type Act = Increment | Decrement

type Fact = Incremented Int | Decremented Int

type Msg = Perform Act | Apply Fact

init : List Fact -> (Model, Cmd Msg)
init facts =
  (List.foldl apply (Model [] 0) facts, Cmd.none)

perform : Act -> Model -> Cmd Msg
perform act model =
  case act of
    Increment ->
      Incremented (model.view + 1) |> factCmd

    Decrement ->
      Decremented (model.view - 1) |> factCmd

apply : Fact -> Model -> Model
apply fact model =
  { history = List.append model.history [ fact ]
  , view =
      case fact of
        Incremented i -> i
        Decremented i -> i
  }

viewAct : Model -> Html Act
viewAct model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , button [ onClick Increment ] [ text "+" ]
    , div [] [ text (toString model.view) ]
    , div [] (model.history |> List.map (toString >> text >> \x -> div [] [ x ]))
    ]

-- boilerplate below here should be in helpers
message = Task.succeed >> Task.perform identity identity

factCmd = Apply >> message

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Perform act ->
      (model, perform act model)

    Apply fact ->
      (apply fact model, Cmd.none)

Seems too heavyweight for the counter example. Although, once I added type annotations and standard program declaration, the counter example was 32 LOC (sans newlines). Compare that to 43 LOC for the messaging + ES code once I move the helper functions out.

But I like the discipline of separating Acts from Facts. And I think the overhead would be worth it in more involved programs.

I also experimented with sending Facts directly from the UI (for cases where an Act would directly translate to a Fact), but the onClick handler looked terrible.

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick (model.view - 1 |> Decremented |> Apply) ] [ text "-" ]
    , div [] [ text (toString model.view) ]
    , button [ onClick (model.view + 1 |> Incremented |> Apply) ] [ text "+" ]
    ]

This would likely lead you to make shortcut functions to reduce the clutter. And at that point, you haven't saved anything over making explicit Act messages.

So the next step for this program would be adding undo/redo. It's not too bad, but I didn't put it in here to keep it focused on messaging + ES. Then after that it would be great to figure out how to actually initialize the Elm program with messages and give them to init to build the model.

kspeakman commented 8 years ago

Ah, programWithFlags would allow you to provide initial events.