These are chat archives for jdubray/sam

25th
May 2016
Jean-Jacques Dubray
@jdubray
May 25 2016 01:33

@jsdw From what I can tell, Elm and Redux are very similar:
Redux actions are Elm messages

type Msg
  = MouseMsg Mouse.Position
  | KeyMsg Keyboard.KeyCode

And the Reducer and the Update function are pretty much equivalent

update msg model =
  case msg of
    MouseMsg position ->
      (model + 1, Cmd.none)
...
This message was deleted

Then there are commands:

Elm commands (Cmd) are how we tell the runtime to execute things that involve side effects. For example:

  • Generate a random number
  • Make an http request
  • Save something into local storage
As I discussed earlier, personally I don't like the coupling between message (redux action) and model. IMHO, an action is some kind of adapter that update the semantics of the event to the model. When you do that in the reducer, I feel that you generate some unnecessary clutter. In essence the entire "propose" phase gets lumped together with the "accept" phase.
Jean-Jacques Dubray
@jdubray
May 25 2016 01:40
The core problem that we need to solve is "mutability of the application state", everything else is secondary. Redux and Elm seem to actually try to suppress mutations. Everything else is a concern except mutation.

In particular, "actions" don't chain, that is totally counter to a healthy mutation:

In Elm we use tasks for asynchronous operations that can succeed or fail and need chaining e.g. do this, then do that. They are similar to promises in JavaScript.

Jean-Jacques Dubray
@jdubray
May 25 2016 01:47

The goal of an action is to propose values to mutate the state. You cannot execute another action until the state mutation has completed. It is nonsensical to do otherwise, or what you call an action is not an action.

If I could succeed at only one thing with SAM, I'd like to succeed at making these semantics widely known to the developer community. Everything else is secondary for me.

devin ivy
@devinivy
May 25 2016 01:51

When you do that in the reducer, I feel that you generate some unnecessary clutter. In essence the entire "propose" phase gets lumped together with the "accept" phase.

whether or not action creators should contain effects aside, i think action creators themselves are absolutely crucial for exactly this reason you've mentioned.

@jdubray how would you test an action that contains an asynchronous effect?
Jean-Jacques Dubray
@jdubray
May 25 2016 02:53
Yes, I know this is the general question I get with SAM and the main rationale. My answer is that things like MounteBank provide a good enough virtualization capabilities (without the need to write mocks) that I don't see what else you need. Ultimately these effects need to happen, your application will have to be tested with them.
Maybe I am missing something but I don't see the light.
I have written a custom service virtualization component using express-http-proxy in 30 lines of JavaScript (node.js). I really don't see the need for anything else.
Jean-Jacques Dubray
@jdubray
May 25 2016 03:23
@devinivy to be frank, I really would like to answer that question once and for all. Unfortunately, Erik Meijer didn't reply to me. I'll try to ping him again.

action creators themselves are absolutely crucial for exactly this reason you've mentioned.

Yes but unfortunately, they are not used by default. I would prefer a model where they are the default, because it rarely makes sense pass a view event directly to the model.

James Wilson
@jsdw
May 25 2016 08:32

@foxdonut @jdubray Interesting feedback, thanks :)

@foxdonut to answer your points, an Elm update does do something in the strict sense, but it could return the model untouched and trigger no actions. idiomatic elm allows for payloads or no payloads. update could look like:

update msg model =
  let updatedModel = case msg of
        NoOp -> model
        Increment -> model + 1
        IncrementBy n -> mdoel + n
  let makeActions newModel =
        if newModel == 5 
           then someEffect
           else otherEffect
  in (updatedModel, makeActions updatedModel)

Here the model does not update itself if the NoOp message is passed to it (however effects may still be generated from the model, which I think is one of the differences you're pointing out; I could pass a "hasChanged" flag back from the updatedModel to prevent makeActions from doing anything, but this isn't standard in Elm!). I've also thrown in examples of passing a payload or not passing one.

I think I'm gradually understanding some of the differences between the elm architecture and SAM. So far they are:

  1. By default, the Elm update function produces a new model AND side effects to run, so the side effects can be based on the message coming in. In SAM, we separate message from effect and make effect depend only on the model state (as I have done above)
  2. An Elm update will always trigger a view update, even if it does not do anything. SAM model.present's can opt to trigger no such update at all. I think partly for me this is just an optimisation thing; if the Elm model does not change via update, the view will also not (and can not) change, so it just comes down to saving some clock cycles in trying to rerender or not.
  3. Following from 2, side effects in elm may be produced whether or not the model changes. in SAM, the model may not change and may not trigger the next step (producing side effects)
@jdubray I'll have a go at drawing up a diagram I think explains Elm a little better than the above when I have a little time, in case it helps make anything clearer. Thanks so much for the feedback so far; I'm gradually making sense of it all :)
Stardrive Engineering
@HighOnDrive
May 25 2016 08:40

@jdubray Saw a new version of the "wires of Bangkok", more React ingenuity in the works. Here's a link but I certainly do not endorse it: http://jlongster.com/Whats-in-a-Continuation

Will look at the sam-tic-tac-toe implementation tomorrow even though it uses React/Redux. IMHO, I think the world would be better off without more redux-xyz patches and broilerplate!

GN :smile:

P.S. This one looks more interesting: https://www.youtube.com/watch?time_continue=6&v=iw2iCMopwkQ
Fred Daoud
@foxdonut
May 25 2016 11:32
@jsdw that is a great summary. I agree with you that saving a few clock cycles should not be the focus. But it's not just that; what about actually refusing an update? What I mean by that: action A triggers an async request that takes long; before the response comes back, the user triggers a cancel action. When the response comes back from action A, it would trigger an update. We have the ability to refuse that update so as not to go into an inconsistent state and confuse the user.
Jean-Jacques Dubray
@jdubray
May 25 2016 16:20
@foxdonut @jsdw what is also important to consider is the case where another action has presented its data and the application state is such that we can no longer present data from an older instance (not to mention things like retries). By keeping track of which action are in flight and allowing actions to present data based on the application state you are in a much better position to implement the logic of complex UIs. The TLA+ semantics give you for free what would otherwise be hard to implement with an Elm model.
James Wilson
@jsdw
May 25 2016 17:10

That's an interesting point! To implement this in Elm, you'd have to do something along the lines of

update msg model =
  let
    -- calculate our new model based on message:
    newModel = case msg of
      CancelLongRequest ->
        { model | performingLongRequest = STOPPED }
      PerformLongRequest ->
        { model | performingLongRequest = STARTED }
      ResultsFromLongRequest res ->
        if model.performingLongRequest == STOPPED
          then model
          else { model | results = res
                       , performingLongRequest = STOPPED }

    -- calculate any side effects to perform based on
    -- new model:
    effects newModel =
      if model.performingLongRequest == STARTED
        then performLongRequestAction
        else Cmd.none
  in
    (newModel, effects newModel)

where basically we keep track in the model of the state of the request, and we can set that state to STOPPED to cancel it early (when the result comes in, it checks for STOPPED and does nothing to the model if it is so)

the problem with the above is that, if we separate deciding which effects to run from model as I have done above, we no longer know whether to kick off the performLongRequestAction or not on subsequent runs of update (because all it has to go on is the thing == STARTED). doing things a more Elmy way, I'd do more like:

update msg model = case msg of
  CancelLongRequest ->
    ({ model | performingLongRequest = STOPPED }, Cmd.none)
  PerformLongRequest ->
    ({ model | performingLongRequest = STARTED }, performLongRequestAction)
  ResultsFromLongRequest res ->
    if model.performingLongRequest == STOPPED
      then model
      else { model | results = res
                   , performingLongRequest = STOPPED }

but that is further from SAM as I understand it as it couples effects and messages again

James Wilson
@jsdw
May 25 2016 17:16
(however, in this sort of case when there has been an explicit request to perform some side effect, It seems cleaner to couple kicking off the effect with the message?)
(we have however kept track of the fact that we're performing a request, so we could easily avoid performing it again etc; I can definitely see the benefit of doing that)
Jean-Jacques Dubray
@jdubray
May 25 2016 22:20
@jsdw Yes, I understand that it can be implemented like this but in the SAFE middleware, I implemented a generic way based on SAM's concept of step.
IMHO, and that's the core of my argument, it takes a lot of effort to implement specific state machines for every cancellation you'd like to do, and that's very common in a SPA.
What I am suggesting is to take the dual view of validating that an action can present its data and by doing so, you can implement these generic "step" based strategies. Once you are there, you are pretty much implementing SAM with Elm. These are the kinds of semantics that I would argue are currently missing in Elm.
Jean-Jacques Dubray
@jdubray
May 25 2016 22:40
This is the "heart" the step concept, as you can see it's really simple:
safe.newStep = (uid,allowedActions) => {
    allowedActions = allowedActions || [ ] ;
    var k = 0 ;
    var step = { 
        stepid: uid , actions: allowedActions.map((action) => {
            var auid = uid + '_' + k++ ;
            return { action, uid: auid } ;   
    })  } ;

    safe.steps.push(step) ;
    safe.logger.info('new step        :'+JSON.stringify(step)) ;
    return step ;
} ;
It is simple because we take the point of view of what's allowed after each mutation of the application state. It does not matter happened prior to that mutation, it gets automatically discarded, unless you choose not to. That's because logically, once you moved one step, you can logically discard anything that could have happened. You cannot go back in time.
Jean-Jacques Dubray
@jdubray
May 25 2016 22:45
A new step is created in the State function along with rendering the State representation and invoking the next-action-predicate:
safe.render = (model,next, takeSnapShot) => {
    ...
    safe.lastStep = safe.newStep(safe.stepCount++, safe.allowedActions) ;
    ...
} ;
When an action is dispatched we enrich the event with the step-action-id:
data.__actionId = a.uid ;

In the model's present method we then check that this action instance is allowed to present data:

lastStepActions.forEach(function(el) {
                if (el.uid === actionId) {
                    presentData = true ;
                }
            });

If the "step" has already been completed by another action instance, then the "late" action's data is simply ignored.

Jean-Jacques Dubray
@jdubray
May 25 2016 22:51
Of course any variation can be implemented, for instance you could still accept data from older steps if it made sense. The newStep function can have any kind of application specific logic.
I believe in the end that the flow propose/accept/learn is key to building modern UX. I am not saying it cannot be implemented with Redux, Elm, Cycle... All I am saying is that these frameworks are passing on very important semantics. Worse, sometimes they are in complete violation of these semantics. SAM is a pattern, not a framework, so I am not trying to compete with frameworks. I built SAFE, just to illustrate the power of SAM (and also because it is so much fun to write this code)
Jean-Jacques Dubray
@jdubray
May 25 2016 22:59
This is what SAFE can do:
  • element wiring
  • action dispatcher
  • session dehydration / hydration
  • enforces allowed actions and action hang back
  • logging
  • server side time travel
Jean-Jacques Dubray
@jdubray
May 25 2016 23:52
@509dave16 I am still going through your code. I need to build a sequence diagram...
David Fall
@509dave16
May 25 2016 23:54
@jdubray Sounds good. I guess I should take UML diagrams into consideration for documentation of repositories.
Jean-Jacques Dubray
@jdubray
May 25 2016 23:58
I like the way you structured your project, but it is still hard to follow.