Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Activity
  • Jul 20 2019 23:24

    jdubray on master

    Adds todomvc-app-angular Adds todomvc-app-angular Adds … (compare)

  • Jul 20 2019 23:24
    jdubray closed #9
  • Jul 20 2019 23:21
    jdubray opened #9
  • Jul 20 2019 23:21

    jdubray on todomvc-app-angular

    Adds todomvc-app-angular (compare)

  • Jul 19 2019 11:06

    jdubray on master

    Small optimization (compare)

  • Jul 19 2019 02:42

    jdubray on vue-todo-app

    (compare)

  • Jul 19 2019 02:42

    jdubray on master

    Refactors the acceptors to use … Refactors the acceptors to use … (compare)

  • Jul 19 2019 02:42
    jdubray closed #8
  • Jul 19 2019 02:42
    jdubray opened #8
  • Jul 19 2019 02:06

    jdubray on vue-todo-app

    Refactors the acceptors to use … (compare)

  • Jul 19 2019 00:32

    jdubray on master

    Adds todomvc-app-vue sample Fixes the checked defect Adds missing files and 3 more (compare)

  • Jul 19 2019 00:32
    jdubray closed #7
  • Jul 19 2019 00:30
    jdubray synchronize #7
  • Jul 19 2019 00:30

    jdubray on vue-todo-app

    Adds methods to edit the task's… (compare)

  • Jul 17 2019 05:03
    jdubray synchronize #7
  • Jul 17 2019 05:03

    jdubray on vue-todo-app

    Cleans up the code (compare)

  • Jul 17 2019 05:00
    jdubray synchronize #7
  • Jul 17 2019 05:00

    jdubray on vue-todo-app

    Adds missing files (compare)

  • Jul 17 2019 04:56
    jdubray synchronize #7
  • Jul 17 2019 04:56

    jdubray on vue-todo-app

    Fixes the checked defect (compare)

littlewhywhat
@littlewhywhat_gitlab
Hi! I am fascinated with SAM :) I came up with a couple of questions while using it.
  1. sometimes I have situations when I want to reuse the logic of one accessor into another - is it ok and a common practice to call one from another?
  2. how to handle actions that doesn't have any data except that they happened - for example, toggle selection of some object or clear selection? I have to somehow identify the action in the proposal so that acceptors could react and so far except of creating some dummy field i didn't come up with anything else. What would you do?
Jean-Jacques Dubray
@metapgmr_twitter
@pfurini thank you, hopefully everything will be ok!
@littlewhywhat_gitlab thanks, me too! I still have a couple of weeks of reprive.
An action sending a proposal, even empty, should trigger the render loop, so you should not need any kind of dummy acceptor.
I define my acceptors as higher order functions, so I can reuse them in different contexts/components:
const { intents } = addComponent({ 
  actions: [
    () => ({ incBy: 1 })
  ],
  acceptors: [
    model => proposal => model.counter += proposal.incBy || 1
  ]
})
Jean-Jacques Dubray
@metapgmr_twitter
Is that what you meant when you say accessor?
I would say it's probably not a good idea to call one from another
Daniel Neveux
@dagatsoin
I would say that the pattern does not enforce you to NOT link acceptor between them. But I would say that is symptomatic for a need of refactoring or a need of separation of concern.
If two acceptors does the same things. Well it is the same acceptor.
Daniel Neveux
@dagatsoin
At the opposite if acceptorC= f(acceptorA, acceptorB), it is up to you to write the code as you whish.
Daniel Neveux
@dagatsoin

For "empty" proposal data. In fact they should not be reaaly empty but contains information on which acceptor(s) to trigger.

{mutations: [{type: "RESET"}]}

littlewhywhat
@littlewhywhat_gitlab

Hi, thank you for quick replies! I got some ideas but maybe let me provide you with a concrete example:

// simple task selection
// - only one can be selected at a time
// - user can clear selection
// - select a task or toggle it (select or unselect)
const selectTask = model => ({ taskIdToSelect }) => on(taskIdToSelect, () => {
    const task = findTaskById(model, taskIdToSelect);
    if (task) {
        model.selectedTask = task
    } else {
        model.errors.push("Can't select the task");
    }
});
const clearSelection = model => ({ clearTasks }) => on(clearTasks, () => {
    model.selectedTask = null;
});
const acceptors = [
    // ...,
    selectTask,
    clearSelection
    model => { taskIdToToggle } => on(taskIdToToggle, () => {
       if (model.selectedTask.id == taskIdToToggle) {
           clearSelection(model)();
       } else {
           selectTask(model)({ taskIdToSelect: taskIdToToggle });
       }
    }
];
const actions = {
    selectTask: (taskId) => sam({ taskIdToSelect: taskId }),
    toggleTask: (event) => sam({ taskIdToToggle: event.target.id }),
    clearSelection: () => sam({ clearTasks: true })
}

So above you can see that I use a boolean in proposal to mark that I want to clear selection and I reuse clearSelection and clearTask acceptors from "toggle" acceptor - this may help me save some lines of code as selectTask is not so trivial. I omit "wiring" - say sam function pass proposal to model and starts the loop.
I would appreciate any feedback on this solution.

Daniel Neveux
@dagatsoin
Appart extracting the toggle acceptor implementation in its own function, I would say: LGTM.
Daniel Neveux
@dagatsoin

Another approach is too think about acceptors as a low level API and actions as high level API.
Example:

  • the littlest mutations the model can do is to select/unselect a task. So you need only to have two mutations
  • the functionalities of your app have more higher concerns: selectMultipleTask, clearSelection, toggle... and all this functionality can be expressed as a proposal with multiple acceptors calls.

So, in this approach, for the clearSelection case, your action can return:

  • [{unselectTaskId: 0, unselectTaskId: 1, unselectTaskId: 4,...]}]

Also, we can imagine that your state function exposes the selectedTaskId to the view/actions, which seems possible in a Todo list app
In this case, you can refactor you toggle action to return either:

  • {unselectTaksId}
  • or {selectTaskId}
Daniel Neveux
@dagatsoin
This has several benefits in a large app:
  • Separation of concern: business code is in the model, functional code is in the view/action
  • extensibility: with this logic, you can implement new functionalities just by writing new actions and without touching the model.
  • maintenance/regression: less surface of code changes at each iteration
Daniel Neveux
@dagatsoin

To reuse a old idea of @metapgmr_twitter Imagine a service were a SAM instance exposes a present function which accepts only a set of the lowest possible mutations.

This would let users to write their own actions in complete independence of the model.

Ex: a RPG game where you can craft anything, a no-code development service, ...

Jean-Jacques Dubray
@metapgmr_twitter
@littlewhywhat_gitlab to build on @dagatsoin's reply I think you should use oneOf in that case, it should look like this:
const selectTask = model =>  (taskIdToSelect) => {
    const task = findTaskById(model, taskIdToSelect);
    if (task) {
        model.selectedTask = task
    } else {
        model.errors.push("Can't select the task");
    }
}

const selectTaskAcceptor = model => 
                         ({ taskIdToSelect, taskIdToToggle  }) => oneOf
                                   (taskIdToSelect, selectTask(model)(taskIdToSelect ))
                                   (taskIdToToggle , selectTask(model)(taskIdToToggle ));
It would be a similar construct for clearTask
Jean-Jacques Dubray
@metapgmr_twitter
in all honesty, the problem in the 70s was not GOTO statements but decision structures: if-then-else being the worst. switch is a bit better but the semantics are quite broken. Worst of all they are sprinkled across your code without any temporal structure.
Even loops to a certain extent ought to be used with extreme care. Redux for instance, was based on the foundation of a reducer: events.reduce(nextStateRelation, initialSate)
Jean-Jacques Dubray
@metapgmr_twitter
where nextStateRelation is a function of the type (state, event) => { ... }
that forces developers to cram the nextStateRelation with all the logic that SAM decouples (actions, acceptors, reactors) and does not have any construct for next-action. Of course, you can use thunks and sagas but it's not natural, they are adjuncts to the reducer pattern.
Jean-Jacques Dubray
@metapgmr_twitter
You really have to be careful about the construct you use, some could be extremely low level (like if-then-else) and should not be used systematically (and hence randomly in your code), some are very specific (like a reducer) and they cannot be applied outside their context without careful considerations.
littlewhywhat
@littlewhywhat_gitlab
@metapgmr_twitter @dagatsoin thank you very much for your answers - the concepts you describe are a bit new to me and even though they seem straightforward, I need some time to digest them and be able to apply in practice :) if I come up with further questions, I will ask
Jean-Jacques Dubray
@metapgmr_twitter
yes, I understand, these higher order functions are a bit of a mind bender, just like everything it requires a bit of practice. The background is that before that kind of code was just a bunch of if-then-else and that was a bit ugly. Functions like on, mon (multiple on) and oneOf are just a wrapper on if-then-else. The trick is that oneOf returns itself so you can just keep calling it without having to write a complete function call, just pure syntactic sugar.
littlewhywhat
@littlewhywhat_gitlab

@dagatsoin I agree with the clearSelection that it could just create a proposal to set selectedTask to null. Regarding thinking about acceptors as low-level API, I end up sometimes with making from acceptors just a set of setters. I am afraid that then they transfer their validation role to actions. Here is another example where I struggle to avoid just passing a dummy boolean to mark the acceptor I want to use:

const sam = (proposal) => { /* sam loop */}

const Actions = {};
const Acceptors = {};
Acceptors.focusOnMyself = model => ({ focusOnMyself }) => {
    on(focusOnMyself, () => {
        if (model.myPosition) {
            model.focusPosition = model.myPosition;
        } else {
            model.errors.push("No my position to focus on");
        }
    });
};
Acceptors.updateFocusPosition = model => ({ focusPosition }) => {
    on(focusPosition, () => {
        model.focusPosition = focusPosition;
    });
}

Actions.focusOnMyself = () => {
    sam({ focusOnMyself: true });
}
Actions.focusOnMapCenter = (map) => {
    sam({ focusPosition: map.getCenter() })
}

I could probably reuse the updateFocusPosition acceptor but then I would have to put the check for myPosition to not be null into the Action and add acceptor to propose the errors. This way it really seems to me that acceptors will be come

that acceptors will eventually become just simple setters... Another thought though I had is to move the logic into some reactor and introduce actually a boolean field on the model to focusOnMyself that would be reset each loop. Then reactor would check for the field and react with error or setting the focusPosition. Could you advise something better in this case?
littlewhywhat
@littlewhywhat_gitlab
I assume that we don't want to make acceptors to be simple setters.
Daniel Neveux
@dagatsoin
This message was deleted
Daniel Neveux
@dagatsoin
@littlewhywhat_gitlab here are some key points of my mental model of "where to put code in SAM".
  • the model is invisible to the view. The state function exposes only what the view need to know about it (I called it ModelRepresentation).
  • the action has not access to the model, so it does not do any validation involving data that are not exposed in the ModelRepresentation.
  • the action has one role:
    • creating a payload that the acceptors should accept
    • the action can do some validation/logic stuff only in the context of the request
    • from the point of view of a SAM app, it is a functional logic
    • ex:
      • from that SAM web site: "check if the password is valid"
      • from a RPG game: compute the power of a fireball
  • an acceptor does two things:
    • look if the payload is coherent with the actual model data
    • set the data
    • from the point of view of a SAM app, it is a business role.
    • ex:
      • from that SAM web site: "cannot use a password that matches any of your last three passwords."
      • from a RPG game: compute the internal damage of the ennemy caused by the fireball

That being said.

I am afraid that then they transfer their validation role to actions.

It the validation is made in the context of the User/View, there is no problem because it is not business concern, but a functional concern.

Daniel Neveux
@dagatsoin

For the map exemple I think I miss some context but :
When I see some acceptors having the same name as the action, it makes me feel about a code anti pattern about separation of concern. (cf low/high level API speech above).
It may works seamlessly but I think it is not future proof because it means that functional logic leaks into business logic.

  • the business role can be resume to set a position:

    • the acceptor is just a setter indeed
    • there is no need to have an acceptor validator for now, but some business rules can be added later: Ex: coarse precision depending on the model configuration, accept only road position, ...
    • the model does not care about from who came the coordinates (user position, center of map, a tap, ...). It just do one thing and does it well: store the focused position.
  • the functional role of your app are the user stories

    • "I can center the map on me"
    • "I can center the map on its bounding box"
    • So, those two stories are just two actions triggered by the view. Each action will call the setPosition acceptor.
littlewhywhat
@littlewhywhat_gitlab
@dagatsoin Daniel, thank you very much, I think I am starting to understand the differences between functional and business logic. I have some more questions regarding implementation:
  1. Is it correct that we have to be sure that the code from the moment we start our validation in Action to the moment we create a new ModelRepresentation is synchronous? That is, in the possible asynchronous actions we should start our validations, once the asynchronous part is done. Otherwise, it seems that we can bring the model to the incorrect state.
    For example, consider the following sequence of events:
    • user interacts with UI and we start the action to focus map to the user's position
    • we validate and create the proposal
    • we run asynchronous action before presenting proposal to the model
    • user's position is updated by another action in the model
    • our async action is done and we present the proposal to focus the map on the old user's position and we get the wrong model state
      To be honest before I thought that all validations, functional or business, are ran in the model to avoid these situations.
  2. Let's say we merge my previous examples and tasks can be selected and have a position on the map. So we would like to make an action that will select a task and focus the map position on this task. If we create one proposal then the problem can happen when the acceptor to select the task fails but another acceptor to set the focus position doesn't. This situation again creates the invalid state. How to avoid these situations?
Thank you
Daniel Neveux
@dagatsoin

@littlewhywhat_gitlab For point 1

we validate and create the proposal
we run asynchronous action before presenting proposal to the model

Do you means to run two differents actions and compose the proposal ? Or just asking something to a validator service, then create the proposal and present it?

Daniel Neveux
@dagatsoin

For point 2
If you are sure that your system can reach an incorrect state because a position is valid and a task not (or inverse):
1 - if you want to use two proposals: chain the actions

  • trigger the first actions to select the task
  • in the NAP (the reactor) launch the second task (focusMap)

2 - if you want to use a single proposal: let the view to known about the invalide state

  • the state function can expose an enum/string/whatever derived from the model to let the view to known about.
  • if so, either you run another function to compensate
  • either you

3- if you use the SAM library, there is a mechanism exactly for this case (look at https://www.npmjs.com/package/sam-pattern#model-checker-1) (Honestly, I did not try, I see this like a transaction compensation but not sure, maybe @metapgmr_twitter can explain you)

littlewhywhat
@littlewhywhat_gitlab

@dagatsoin 1. I think I mean partially the validation service option. Maybe let me try to give a more concrete example. Let's say, we have a user story - "I can see the path from my position to the other location by specifying the location's name". Business rule could be that the path shouldn't leave some perimeter. So the code could be like this, after what we discussed above:

// Actions
const showPathToLocationByName = async (locationName) => {
    // 1. async call to some map REST API to get location coordinates by its name
    // for example, "Paris" would resolve with array [ 48.85, 2.35 ]
    const location = await getLocationCoordinates(locationName);
    // 2. get my position somehow from the ModelRepresentation. Actually how? Let's say we have some singleton modelRepresentation.
    const myPosition = modelRepresentation.state;
    // 3. Validate my position
    if (myPosition !== null) {
        // 4. send proposal to set a path on the map as an array of map points
        sam({ pathOnTheMap: [ myPosition, location ] });
    } else {
        sam({ error: "Can't build path without my position" });
    }
}

// Acceptors
const setPathOnTheMap = (model) => ({ pathOnTheMap }) => {
    // 5. validate that path is in the perimeter configured before
    if (isInThePerimeter(model, pathOnTheMap)) {
        // 6. set the proposed path on the map
        model.pathOnTheMap = pathOnTheMap;
    } else {
        model.errors.push("Path is outside the defined perimeter");
    }
}

I suppose that this is the way with keeping model with low-level API. We just have a simple setter that we use from our action.

My point from above is that we need to be sure that steps 2 -> 6 are run synchronously. If not, then our validation in action (3) may not be longer true once we set the path in the model (6).

And then we can't actually provide "myPosition" as a parameter to the action. We have to get it after async call (1) is done. So another question is how we will get myPosition - unless we provide the access to the current state in actions.

So could you say if it's correct that 2 -> 6 has to be run synchronously in this case. If yes then what could be the ways to get "myPosition" in action to validate it? Thank you

for 2. yes I also I was thinking about NAP as a way to go. Thank you for these options
Daniel Neveux
@dagatsoin
  • In SAM the synchronicity of action -> acceptors is not mandatory. @metapgmr_twitter gives a good example somewhere on a blog, where the asynchronous action "Save" it hit, then the action "cancel" is hit. And the first action to present their proposal wins.

  • Clarification on a previous point:
    Here, the ModelRepresentation is the View. So the view can be aware of the actual position. (View = f(Model))

About the SOC in your example:

OK:

  • having a setPathOnTheMap is low level enough for a map model. The model can store this as a GeoJSON object.
  • asking to the app to show the travel is high level enough. Great!
  • It is totally find to have an asynchronous call in your action.

Points to consider:

  • sending an error like "Can't build path without my position" is OK if its fits the spec requirement of you app to display such error. If not I would let pass an undefined position and let the model handle the error. ("A path needs at least two points")
  • An action can totally uses external services if its purity is not an issue for your case. Personnaly, I prefer to keep action pure (and testable) and calling service in the view then pass the result into the action. So you can call a GPS service in the view then pass the position to the action
Daniel Neveux
@dagatsoin
[Free ADD :) ]I just published "Wizar devlog 02: Reference implementation" https://dev.to/dagatsoin/wizar-devlog-02-reference-implementation-40de
littlewhywhat
@littlewhywhat_gitlab

@dagatsoin thank you again for the answer. Still I think there are problems in the way we implement this example or maybe I am missing something. I didn't find in @metapgmr_twitter texts the example you point out about "the first proposal wins". At sam.js.org I found only phrase:

another action executed concurrently would be able to present it's proposal as if the first (long running) action would have never been triggered

I understand it that Model should be able to correctly handle proposals no matter what order they come in. But i don't see here - first comes wins. In your example I would expect that Cancel coming first will just fail as there is nothing to cancel yet. Therefore I think we should be careful in splitting validation logic from model to the action to be sure that the rule above will still be true.

I will try to demonstrate that in my example that this rule won't work. I tried to apply some of your recommendations below (the one about getting myPosition - now it is a parameter to the action and is received from the view). I also add action to update myPosition and acceptor-setter:


// Actions
const showPathToLocationByName = async (fromPosition, locationName) => {
    if (fromPosition !== null) {
        const location = await getLocationCoordinates(locationName);
        sam({ pathOnTheMap: [ fromPosition, location ] });
    } else {
        sam({ error: "Can't build path without my position" });
    }
}
const setMyPosition = (location) => {
    sam({ myPosition: location });
}

// Acceptors
const setPathOnTheMap = (model) => ({ pathOnTheMap }) => {
    if (isInThePerimeter(model, pathOnTheMap)) {
        model.pathOnTheMap = pathOnTheMap;
    } else {
        model.errors.push("Path is outside the defined perimeter");
    }
}
const setMyPosition = (model) => ({ myPosition }) => {
    model.myPosition = myPosition;
}

Now let the following sequence happen:

  1. Model has myPosition is not null - let's say it's equal to location A
  2. Actions.showPathToLocationByName(myPosition, 'B') is invoked and as myPosition is not null, it is waiting for getLocationCoordinates to resolve
  3. Actions.updateMyPosition is invoked with location C
  4. Model accepts { myPosition: locationC } proposal and sets model.myPosition to location C
  5. getLocationCoordinates resolves and Actions.showPathToLocationByName proposes { pathOnTheMap: [ myPosition, locationName ] } where myPosition is still a location of A
  6. As Model doesn't know anything that it's something about myPosition and doesn't check this integrity. It accepts the proposal and sets the pathOnTheMap to [ A, B ]
  7. The Model is in invalid state and it renders to the user the path from the user's old position A to the point B. In reality though the path should be from C to B.
Daniel Neveux
@dagatsoin

@littlewhywhat_gitlab I got your point.
To clarify, we need to introduce another fondamental concept of SAM: step.
A step is unit of time. It is the smallest operation of you app (in a functional point of view).
As a unit of time you can image a step to have a uniq id (a timestamp, un number, whatever...)
A step starts when an action is triggered and finish when the state representation is computed.
During the first phase of a step, it is possible to fire multiple actions. Like in your example (shoPathToLocationByName and updateMyPosition)
The end of this first phase is set when an action is the first to present its proposal. At this moment, the SAM loop is locked and won't accept any other proposal for this step.
So, in your situation, if the updateMyPosition present its proposal first, the later presentation of the proposal of shoPatchToLocationByName will be cancelled.

A way to implement this is too add a tag to your action arguments. There is a implementation example here : https://github.com/jdubray/sam-safe/blob/master/safe.js

Also if you use https://www.npmjs.com/package/sam-pattern#asynchronous-actions there is already a mechanism for this.
Jean-Jacques Dubray
@metapgmr_twitter
:+1:
In a lot of cases, we do not implement concurrent actions, but when you need to do so, SAM provides an elegant solution (at least I believe so) that Daniel detailed very well.
the sam-pattern library hides all the details for you.
that's precisely what temporal programming is about, have a well defined step (as so eloquantly defined by daniel) and reason with great precision as to what happens in relation to that step. What's great is that the program can do all kinds of crazy stuff (like starting multiple actions in parallel) and the step makes sense of it. Yet, you can have several SAM instances running and communicating with each other, again with great precision.
Jean-Jacques Dubray
@metapgmr_twitter

@littlewhywhat_gitlab

I assume that we don't want to make acceptors to be simple setters.

From a temporal perspective, the action has only knowledge of a prior state representation. It doesn't know how many steps the model is ahead from the point in time the state represention from which the event/action originates. Or if the action is async, again, it does not know how many steps occurred. That's really essential to keep in mind. So the action should be focused on validating and enriching the event (say with an API call) and should only contain data from the state representation. It should never require knowing a current property of the model (sometimes it's a bit challenging, but that rule should not be violated). The model is responsible for maintaining the integrity of the application state and its logic should be relatively simple and close to setter like logic. Very often you would have properties of the event that impact multiple properties of the model. Actions and acceptors have a many to many relationship. An action can trigger multiple acceptors and an acceptor can be triggered by different actions.

When the action's role is to fetch data from an API call, it passes the response or part of the response to the model that decides whether and how to accept it, then the state representation could further transform it, if that makes sense.