These are chat archives for jdubray/sam

21st
Nov 2016
Edward Mulraney
@edmulraney
Nov 21 2016 20:50
@jdubray do you think that actions should model events that have occured or intentions?
TODO_ADDED vs ADD_TODO
Jean-Jacques Dubray
@jdubray
Nov 21 2016 20:57
the concepts are:
  • events (mouseClicked,...)
  • intent (submitForm)
  • action (e.g. purchase Order Items)
  • update model
In general an action would be a verb and unrelated to the event that triggered it
intent and action are overlapping, though you could see them as distinct
Edward Mulraney
@edmulraney
Nov 21 2016 20:58
so when a user submits a todo form, has he performed an action: ADD_TODO or has an event occured TODO_ADDED, or is it an intention: TRY_ADD_TODO
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:00
The event that you are talking about is actually a "state" (once the model has mutated) then an event can be emitted. An event is "the occurence of a state", that's the official definition of an event
Edward Mulraney
@edmulraney
Nov 21 2016 21:00
when a mouse is clicked an event occurs but there may be no state change
i.e. no model mutation
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:01
yes, that's the occurence of a state on the mouse side.
the "TODO_ADDED" is truly on the State side
The action would not know of the resulting state (todo_added)
Edward Mulraney
@edmulraney
Nov 21 2016 21:02
so the model should be able react to events AND be pro-active and publish events?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:02
that's the general confusion SAM is trying to clear up, an action should be unrelated to the State
well, not quite since the model is triggered by "present" it does not know about the event that resulted in presenting the proposal
That would be too much coupling
I would argue the Model should know about the States either, it is the role of the state function to notify the "learners" by whichever mechanism
Edward Mulraney
@edmulraney
Nov 21 2016 21:05
so we should present an intention: ADD_TODO and the state function should compute a status: TODO_ADDED,
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:06
In general I would say yes, though in this case, it would be hard for the state function to "compute" that value, the model would have to give a hint.
Let's say there is another state which could be "too_much_todos"
that would be computed by the state function
Edward Mulraney
@edmulraney
Nov 21 2016 21:06
sure
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:06
the model should not know much about what you do with the list of todos
Edward Mulraney
@edmulraney
Nov 21 2016 21:08
so in my app's state function, how can i work out if a todo was sucessfully added ?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:08
the model would have to keep track of that value
Edward Mulraney
@edmulraney
Nov 21 2016 21:08
i suppose i'd have to track last_added_id, vs latest(id)
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:08
that state cannot be computed
but that would mean the state function is stateful
it's better to keep it stateless
Edward Mulraney
@edmulraney
Nov 21 2016 21:09
last_added_id would be in the model
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:09
yes
Edward Mulraney
@edmulraney
Nov 21 2016 21:09
making it stateless
Edward Mulraney
@edmulraney
Nov 21 2016 21:16
conceptually, how would i debounce sam actions? say i have list of users, clicking a user tries to fetch their details, the user clicks like mad on lots of usernames - i want to ignore all but the last request
must be similar concept to action cancelling or throttling
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:26
One way to do it is to issue an action instance id for each action triggered, then present that value to the model, no state is rendered (you might use a different present method for that).
then you only accept a proposal from the last action instance.
would that work for you?
Edward Mulraney
@edmulraney
Nov 21 2016 21:31
would i need to store the promise in the model so i can cancel it?
does nap belong before render or after render?
Edward Mulraney
@edmulraney
Nov 21 2016 21:44
what are the implications of performing async in the model instead of actions? it seems like the model semantically should be responsible for performing the required work
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:44
Not that I can think of
In general the async you want to do in the model are "fire and forget" (no impact on the state representation, just the application state)
You can also re-render in case of error.
Sorry, my answer is a bit messy
Edward Mulraney
@edmulraney
Nov 21 2016 21:46
so fetch users list for example - fire, store, forget ?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:46
No, reads fit well in the actions
not in the model
Edward Mulraney
@edmulraney
Nov 21 2016 21:47
but then we end up with a broken action system
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:47
Updates typically belongs to "next-action" but you have two choices
Edward Mulraney
@edmulraney
Nov 21 2016 21:47
one behaviour for synchronous actions, another behaviour for asynchronous
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:47
1/ synchronous update
2/ asynch update with a second render (it is equivalent to next-action / present)

To answer your question,

one behaviour for synchronous actions, another behaviour for asynchronous

no, it works in both cases

because it does not matter when the proposal is passed to the model
There is no requirement for this proposal to occur synchronously or asynchrously
of course you have to be careful that the "state" is such that the action can be triggered and the proposal can still be accepted
but there is no requirement on the action
Edward Mulraney
@edmulraney
Nov 21 2016 21:56

lets take our understanding of the two types of action:

sychronous:

function incrementCounter() {
  present({type: "INCREMENT_COUNTER"})
}

asynchronous:

function fetchUsers() {
  present({type: "FETCH_USERS_REQUESTED"})
  ajax.get("http://asdf.com/users")
    .then(users => present({type: "FETCH_USERS_SUCCEEDED", users}))
    .catch(error => present({type: "FETCH_USERS_FAILED", error}))
}

if we wanted a consistent API we could make incrementCounter like this:

function incrementCounter() {
  present({type: "INCREMENT_COUNTER_REQUESTED"})
  modelAccessor.get("counter").set(counter => counter + 1)
    .then(users => present({type: "INCREMENT_COUNTER_SUCCEEDED"}))
    .catch(error => present({type: "INCREMENT_COUNTER_FAILED", error}))
}
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:58
in general you don't need to keep track of what the action is doing
Edward Mulraney
@edmulraney
Nov 21 2016 21:59
the two incrementCounter approaches have larger implications on the architecture, but we're okay with doing this for api calls for some reason?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 21:59
To debouce you could do something like that:
function fetchUsers() {
  present({type: "FETCH_USERS_REQUESTED", instanceId: newInstanceId()})
  ajax.get("http://asdf.com/users")
    .then(users => present({type: "FETCH_USERS_SUCCEEDED", users}))
    .catch(error => present({type: "FETCH_USERS_FAILED", error}))
}
Edward Mulraney
@edmulraney
Nov 21 2016 21:59
how would that debounce?
what does the handler look like?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:03
In general, I would prefer the action to never access the model
Edward Mulraney
@edmulraney
Nov 21 2016 22:03
agreed - but its interesting what happens when you compare approaches to actions
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:03
In the model you would only accept the proposal that has the latest instanceId
sorry, forgot to add the instanceId to the second proposal
Edward Mulraney
@edmulraney
Nov 21 2016 22:04
surely the latest instanceId is always the one i the proposal?
meaning you'd be accepting every request as it comes
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:04
It would look like this:
function fetchUsers() {
  let instanceId = newInstanceId() ;
  present({type: "FETCH_USERS_REQUESTED", instanceId})
  ajax.get("http://asdf.com/users")
    .then(users => present({type: "FETCH_USERS_SUCCEEDED", users, instanceId}))
    .catch(error => present({type: "FETCH_USERS_FAILED", error}))
}
Edward Mulraney
@edmulraney
Nov 21 2016 22:06
ah so it instead of cancelling the request we just ignore the result
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:06
In the model you would store instanceIds:
instanceIds[proposal.type] = proposal.instanceId;
yes, it would be too hard to cancel the request
I am not sure you save much on the server or client by cancelling
Edward Mulraney
@edmulraney
Nov 21 2016 22:08

re: async in action vs in model
if we put the async into the model, then the action api would be consistent:

const incrementCounter = () => present({type: "INCREMENT_COUNTER"})
const fetchUsers = () => present({type: "FETCH_USERS"})

corresponding units of work/mutations:

if (proposal.type === "INCREMENT_COUNTER") mode.counter += 1
if (proposal.type === "FETCH_USERS") ajax.get("http://asdf.com/users").then(users => model.users = users).catch(error => model.usersError = error)
i agree that putting async in model is worse than in the action, but something doesn't feel right about either approach.
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:09
yes, the problem would be when you render
it is not wrong to do it in the model
but I prefer having the actions running the queries
Edward Mulraney
@edmulraney
Nov 21 2016 22:10
we could make presnt asynchronous and then render on .then
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:10
The advantage of doing it in the action is that you don't have to worry when you render
sure, but what happens if another action wants to present data to the model
Edward Mulraney
@edmulraney
Nov 21 2016 22:10
model.present().then(() => render(state(model))
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:10
I know
Edward Mulraney
@edmulraney
Nov 21 2016 22:10
ah
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:10
but it's kind of blocking the model
It's ok to render twice, on exit, the first time, and as you get results the second time
as I mentioned, it is equivalent to doing a next action/present proposal
Edward Mulraney
@edmulraney
Nov 21 2016 22:13
yeah i mean this is pretty much the same way redux tried to solve it with redux-thunk, which is fine if you dont mind imperative code and passing present/dispatch all the way down. it comes with its own issues. im trying to find a better solution
a better example than fetchUsers might be sendEmail
where sendEmail is not an automatic action, and wouldnt go in nap
i would like to have async in SAM without passing present down
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:15
Sorry, I am having trouble understanding
Edward Mulraney
@edmulraney
Nov 21 2016 22:16
so in SAM if you wire your components you dont have to pass present.
e.g.
function incrementCounter() {
  return {
    type: "INCREMENT_COUNTER",
    payload: {
      by: 2,
    }
}
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:16
You can do that too! nothing says the action has to present anything to the model
Edward Mulraney
@edmulraney
Nov 21 2016 22:16
i know
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:16
It simply means the application state is not interested in the result of that action
Edward Mulraney
@edmulraney
Nov 21 2016 22:17
i know - ive done this - what im saying is, this doesnt work for async where you explicitly need present/dispatch
function sendEmail(present) {
  present({type: "SEND_EMAIL_REQUESTED"})
  ajax.post("http://asdf.com/email")
    .then(success => present({type: "SEND_EMAIL_SUCCEEDED", success}))
    .catch(error => present({type: "SEND_EMAIL_FAILED", error}))
}
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:18
You lost me a bit, what does not work?
Edward Mulraney
@edmulraney
Nov 21 2016 22:18
inside sendEmail i had to include present. inside incrementCounter i didnt need to include present
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:20
Sorry, if you want the counter to be incremented, you'll have to present it to the model
I was thinking more like "sendEmail"
You could only present on error
Edward Mulraney
@edmulraney
Nov 21 2016 22:20
no that's not it -
im saying for my synchrnous action i dont need to explicitly include present because it can be wired
but i dont see how you can wire asynchrnous action
you have to explicitly pass present to the asynchrnous action
do you see what i mean?
the only way i see is to make some special unit of work:
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:26
sure, I see, I don't care as much about the wiring, whatever works
Edward Mulraney
@edmulraney
Nov 21 2016 22:26
it can be important
developer experience is important, same as declarative/imperative
important to some more than others
but developers generally are always looking for the better solution
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:31
yes, I understand, but you'll always run into edge cases.
Edward Mulraney
@edmulraney
Nov 21 2016 22:34
function sendEmail() {
  return {
    type: "XHR_REQUEST",
    payload: {
      type: "SEND_EMAIL",
      url: "http://asdf.com/email",
    }
}
i suppose a generic async request handler like that would work
Zach Dahl
@schtauffen
Nov 21 2016 22:34
a solution like sagas/epics/thunk for redux, but without managing its own state solves the issue somewhat... that is what I tried doing with jackalope/async
I've been deliberating about whether having actions require present as an argument (preferably curried) . I cannot decide. It allows flexibility, like in your async example but can be kind of a pain.
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:36
I don't see the big advantage of this type of action, it's just a request to perform an action
It's clear that the flow is action->update->render, no matter how you "wire" events, intents, requests...
I just don't buy into "declarative effects"
Edward Mulraney
@edmulraney
Nov 21 2016 22:37
the advantage is that the developer no longer has to repeat himself and manually wire "X_REQUEST", "X_SUCCESS", "X_FAILURE". its the same pattern, and also the action no longer needs to be aware of present
Zach Dahl
@schtauffen
Nov 21 2016 22:37
^
Edward Mulraney
@edmulraney
Nov 21 2016 22:39
the action has no business being present aware. its not intersted in how it gets data to the model, its interested in what data
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:39
sorry, I don't see it, I'll need to look at some code. In SAM's case you'll present success or failure to the model
Edward Mulraney
@edmulraney
Nov 21 2016 22:39
but it would be happening inside the unit of work for XHR_REQUEST automatically
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:39
sure, that's just wiring, I have no problem separating the wiring from the implementation

but it would be happening inside the unit of work for XHR_REQUEST automatically

That's the part I disagree with

Edward Mulraney
@edmulraney
Nov 21 2016 22:40
i thought we agreed the model can perform async?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:40
yes, it can, but it's not desirable
it's much cleaner if the action does it
Edward Mulraney
@edmulraney
Nov 21 2016 22:40
cleaner by what measure?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:41
The model controls the "step" so you can decide whether a proposal can still be presented or not
here you are breaking the pattern by moving the proposal inside the model
It's just you can't play with semantics for the sake of convenience
Edward Mulraney
@edmulraney
Nov 21 2016 22:42
im not
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:42
it's not neutral
Edward Mulraney
@edmulraney
Nov 21 2016 22:42
lets discuss that
1. here you are breaking the pattern by moving the proposal inside the model
are you saying the model can never propose values to itself?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:43
It's not the intent of the pattern
Edward Mulraney
@edmulraney
Nov 21 2016 22:43
then nap breaks the pattern
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:43
no, this is a "next-action", it breaks absolutely nothing
Edward Mulraney
@edmulraney
Nov 21 2016 22:43
but it belongs to the domain of the model?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:43
?
an automatic action is triggered once the state has been updated and it is has been determined/computed that the action is a next action
automatic actions are a fact
The problem is that generally programming models completely ignore them
Because there are not that many, we just sweep them under the rug
Edward Mulraney
@edmulraney
Nov 21 2016 22:45
say i create a new function to help me not repeat myself:
function ajaxGet(type, url)  {
  present({type: type+"_REQUEST"})
  ajax.get(url).then(result => present(type+"_SUCCESS", result).catch(error => present(error)
}
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:45
yes
Edward Mulraney
@edmulraney
Nov 21 2016 22:46
so now my email action looks like:
function sendEmail() {
   ajaxGet("SEND_EMAIL", "http://asdf.com/email")
}
is that okay?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:46
yes
Edward Mulraney
@edmulraney
Nov 21 2016 22:47
ajaxGet belongs to the model and uses model.present
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:47
well, yes and no.
ajaxGet does not have access to the model properties
So far, you have just a different wiring mechanism
Edward Mulraney
@edmulraney
Nov 21 2016 22:48
okay so we limit it to only having model.present and not model.data
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:48
it's not so much the important aspect
Edward Mulraney
@edmulraney
Nov 21 2016 22:48
im getting there - 1 sec
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:49
the important semantics is that:
1/ the action has no access to the model properties
2/ only the model can mutate model properties
Your code meets these constraints
therefore it is an "action", not part of the model
Edward Mulraney
@edmulraney
Nov 21 2016 22:50
if i use the same ajaxGet, with it's restrictions of only having model.present, but i let it live inside the handler for the action: XHR_REQUEST, what rules has it broken?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:50
as long as you don't break these rules you are using SAM and the semantics of action / model
The way your code is wired is irrelevant to the semantics
Edward Mulraney
@edmulraney
Nov 21 2016 22:54
okay so a middleware eco system could emerge in SAM too
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:54
I could put everything in a single function or yes, I could use Pub/Sub
programming model, wiring and architecture should all be independent
Edward Mulraney
@edmulraney
Nov 21 2016 22:55
agreed
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:55
EJB being the worst example
programming model, wiring and architecture are one and the same
Edward Mulraney
@edmulraney
Nov 21 2016 22:55
hence why im trying to find an action specification that works independent of the SAM implimentation/flux loop/etc. ;)
Jean-Jacques Dubray
@jdubray
Nov 21 2016 22:55
That's why Spring emerged and eventually won
As I said, the role of the action is to translate an intent into a proposal, nothing else nothing more. As a corollary, actions cannot access model property values.
That's the programming model
sometimes you can pass on the intent and wire the event directly to an action
sometimes, the action does not thing, pretty much presents the event as is to the model
The programming model though require that the action presents the proposal to the model. It's not important how this is wired, but semantically only an action can present a proposal to the model.
You can't have an action present a proposal to another action
Action composition is not part of the SAM programming model, function composition can be used to create a composite action
Edward Mulraney
@edmulraney
Nov 21 2016 23:01
do you have an example
whats wrong with an action presenting to another action? what will go wrong?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 23:04
I thought it was you who showed how you can use two Actions to compose as one.
function compositeAction(intent, present) {
          present = present || model.present ;
         action1(intent,action2) 
}
assuming both action1 and action2 are implemented as:
function actionX(intent, present) {
          present = present || model.present ;
          ...
Edward Mulraney
@edmulraney
Nov 21 2016 23:08
i ca simplify my debounce/cancel etc. by using rxjs
like:
// action creators
const fetchUser = username => ({ type: FETCH_USER, payload: username });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });

// epic
const fetchUserEpic = action$ =>
  action$.ofType(FETCH_USER)
    .mergeMap(action =>
      ajax.getJSON(`https://api.github.com/users/${action.payload}`)
        .map(fetchUserFulfilled)
    );
i remember you thought this wasn't valid in SAM - is that right?
Jean-Jacques Dubray
@jdubray
Nov 21 2016 23:12
sorry have to go, but yes it sounds right
Edward Mulraney
@edmulraney
Nov 21 2016 23:17
okay appreciate the discussion - thanks
Edward Mulraney
@edmulraney
Nov 21 2016 23:27
@schtauffen your SAM implementation is cool - very similar to my first attempt at a SAM loop. i'm trying to write another one now with a better understanding of SAM/flux/etc.
but not being able to declare async actions is really bugging me. im not getting something