These are chat archives for jdubray/sam

29th
Jan 2018
Dmitry
@agDmitry
Jan 29 2018 10:10

So, i starting to implement SAM in my current app. Since React is absolutely in love with immutable data structures (and i myself prefer this approach) i wanted to ask, is there any caveats with implementing SAM in immutable way?

P.S.: One thing that scares me a little, is that SAM looks suspiciously simple. Redux seemed pretty simple and was like a silver bullet, in theory. On practice singletone-store is a pain on big apps, and multiple stores is not so much less pain, because you might find yourself in a situation when you need to synchronize this stores one with another.

Jean-Jacques Dubray
@jdubray
Jan 29 2018 12:47
@agDmitry no, not at all, it's just don't have to. There is some performance penalty in doing so.
IMHO, the reasons why Redux is a pain are:
  • API calls are an after thought in React and the best practice implementation is unecessarily complicated because Redux insists on the reducer being a pure function
  • Actions factoring is non-sensical
  • There is no State function in Redux (selector)
Redux is just a function wrapper on a bunch of code that do not belong together, that's why it is a mess, or even if you break it down, it won't work as well.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 12:54
SAM's factoring is very clear, there are three buckets: actions, model, state (nap)
  • actions are responsible for validating, enriching (API call) and transforming an event into a proposal
  • the model is solely responsible for mutating the application state (again, please note the many-to-many relationship between actions and acceptors, that is a big simplication)
  • the state function computes the props from the model
  • nap computes if there is any next action and exectutes it
In Redux actions are just events, data structures. You need to use action creators, systematically.
@devinivy is using something like reselect to implement the state function.
From my experience, I have been using SAM for two years now, the complexity remains constant. I can always reason about the flow, SAM's flows are (more) predictable because of this factoring, unlike Redux.
Fred Daoud
@foxdonut
Jan 29 2018 13:00
@devinivy thanks for linking to Meiosis, I appreciate it!
@jdubray
Please not Meiosis couples actions with Model updates which, IMHO is an anti-pattern.
Please do not say this as it is untrue.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:02
could you please detail
Fred Daoud
@foxdonut
Jan 29 2018 13:04
Actions propose updates, they do not update the model.
The acceptor updates the model.
I learned that from your wisdom :)
So it hurts a little bit that you say "Please not Meiosis" :cry:
Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:06
This is an anti-pattern:
const createActions = update => ({
  increase: () => update(model => {
    model.value = model.value + 1;
    return model;
  })
});
That is Redux 101
An action must not and cannot (in SAM) modify the model, there is no way to know for the action what the model property values will end up being.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:11
@agDmitry The other key decoupling in SAM, is the decoupling between mutation (model) and state, i.e. the state that an observer sees. Again, that greatly simplifies the code and reasoning about that code. When you look at Redux, all these concerns tend to be implemented inside the reducer, only the APIs are dealt with outside. IMHO, that's why Redux is so painful, it's an unatural factoring.
Fred Daoud
@foxdonut
Jan 29 2018 13:12
The action is proposing a model update. The acceptor is in charge of applying it (or not).
The difference between the action proposing some data and the acceptor modifying the model with that data, vs the action proposing a function, is an implementation detail IMHO. I have found the latter to be much more powerful and considerably reduce boilerplate. The essence of SAM is nevertheless preserved.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:18
How is this line of code not modifying the model directly?
model => { model.value = model.value + 1 }
Fred Daoud
@foxdonut
Jan 29 2018 13:19
It's inside a function that is being passed to the acceptor. If the acceptor does not apply the function, the code is not executed.
Further, when the acceptor applies the function, it is in control of passing the model (the latest model!) to the function, and is also in charge of what to do next with the updated model (like state representation). ← again, credits to SAM :100:
Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:32
I see, I still don't like it much, I prefer something more linear. it is in control of passing the model is somewhat backwards and breaking some of the temporal aspects of SAM. An action is based on the knowledge of the model at a time when the state representation was computed. It's not a good thing to get the latest version of the model to compute the proposal.
@foxdonut how does meiosis deal with API calls?
One of the key aspects of SAM is that the critical section is reduced (pun intended) to the model only. I can trigger several long running actions in parallel and let them do their work, until they are ready to propose. When the model is in control (temporally) of computing the proposal, you break that very important aspect.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:38
I don't mind when people find alternatives to SAM, or come up with their own pattern, but it's not like I cooked up the semantics of the pattern, I tried to derive them from something that's time tested (:-).
Fred Daoud
@foxdonut
Jan 29 2018 13:39
@jdubray Meiosis deals with API calls in the same way as SAM does. As you said, several long running actions in parallel can do their work until they are ready to propose. So for example an action would trigger an async API call, and upon receiving the response, can send its proposal.
I have spend a lot of time working on this pattern, and I owe you a tremendous amount of credit for SAM because it is its principles that I am applying. The pattern has worked out fantastically well for me (and others who have told me) because of its simplicity and elegance, not to mention how it enables you to write plain regular JavaScript code instead of depending on a slew of libraries or some magical annotations on your model. Finally, it is very flexible, you can apply the pattern in a variety of ways, build your modules as you prefer, passing sub-sections of your model, etc. Even mutation or immutable (I know you are not fan) is a choice that the user can make according to their preference, the pattern works well with both.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:45
ok, maybe I need to see more proposals. Not sure how the model being in control can results in the action sending a proposal. I just don't like the model being in control, it just doesn't sound right. In SAM an action can propose to more than one model if necessary.
Fred Daoud
@foxdonut
Jan 29 2018 13:46
Finally the single-source of truth (Model) and view = function(model) (or state) are also the foundation of Meiosis. Its Tracer (time-travel dev tool) is a testament to v = f(M) because you can not only travel back-and-forth through model snapshots, you can even enter your model in the textarea and see the resulting view. This is even more powerful than the redux devtool which instead replays actions, if I'm not mistaken.
Having a separate state object that is tailored to the view, where the model is not necessarily a 1:1 correspondence to what the view needs, is also very simple with Meiosis and it fits the pattern extremely well.
Dmitry
@agDmitry
Jan 29 2018 13:48

Here is what i understand so far:

  • Action is function that:
    • Computes proposal, that then would be presented to model, from some context-specific data.
    • Can invoke side-effects.
    • Knows nothing about model.
  • Proposal has some interface that doesnt correspond to model data interface.
  • Model validates proposals to keep its data integrity.

Here is some example:

const counterModel = {
  count: 0,
  present ( proposal ) {
    if ( 
      typeof proposal.incrementBy === 'number' &&
      proposal.incrementBy > 0
    ) {
      counterModel.count = counterModel.count + proposal.incrementBy;
    }
  },
};

function increment ( event, present ) {
  present( { incrementBy: event.target.value } );
}

increment( event/* Some DOM event */, counterModel.present );

From here i can see that action is bound to model by proposal interface;

Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:52
@agDmitry I would say:
  • The action doesn't know the entirety of the model or how much the proposal will impact the model mutation. You could propose a single value that will result in mutating 10 others.
  • From a temporal perspective, the action cannot access the latest snapshot of the model. It is limited to the knowledge of the model at a time when the state representation from which the event is triggered
When you can the proposal should be expressed in terms of model property values, the values, you want the model to accept. Actions decouple event structures from the model, there is not a need to decouple the proposal structure from the model structure.
However, as I mentioned, the proposal is not an "update" to the model, the model will look at the proposal and decide if and how to accept it, reject it or partially accept it.
SAM is reactive, once the proposal is passed to the model, nothing is returned to the action, the action's lifecycle is complete.
I don't do too much proposal validation, because the action/model work together:
if ( typeof proposal.incrementBy === 'number' && proposal.incrementBy > 0 )
Dmitry
@agDmitry
Jan 29 2018 13:58

So this:

const counterModel = {
  count: 0,
  present ( proposal ) {
    if ( typeof proposal.count === 'number' ) {
      counterModel.count = proposal.count;
    }
  },
};

function increment ( event, present ) {
  present( { count: event.target.value } );
}

would be more correct? Here we can reuse action on some model with compatible data interface.

Jean-Jacques Dubray
@jdubray
Jan 29 2018 13:59
yes, exactly, the building blocks of SAM (actions, acceptors, reactors) are highly reusable.
You can even use 3rd party actions with an OAuth token.
Dmitry
@agDmitry
Jan 29 2018 14:01

Can you be more explicit about what's wrong with this:

if ( typeof proposal.incrementBy === 'number' && proposal.incrementBy > 0 )

I dont quite understand the part about

because the action/model work together

Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:02
It's not wrong per say, but in general you should make sure the action sends valid proposals
The way I implement the many-to-many relationship between action and model is I test the content of the proposal, it's not pretty, I am open to other ways
if (proposal.incrementBy !== undefined) { ... }
Dmitry
@agDmitry
Jan 29 2018 14:04
But action doesnt know what would be valid from data-integrity perspective, because it doesnt know much about model.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:04
That way complex proposals (say a purchase order with line items) will trigger multiple acceptors
@agDmitry yes, absolutely, but still it should know about the proposal, what the action doesn't know is how the proposal will be mutating, as I mentioned, it could mutate many more properties than the proposal itself and trigger several acceptors.
Dmitry
@agDmitry
Jan 29 2018 14:06
Currently i'm using TypeScript and at least can be sure that type of data in proposal is correct.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:06
The proposal itself is a well known contract between the action and the model, so the action is responsible for the proposal to be correct.
Yes, that works.
For me the key is to be sure that however you implement it, you will be able to trigger more than one acceptor per action
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:11
When you cannot do that, you tend to duplicate code or create a complex flow. SAM's flow is pretty simple
event -> action -> acceptor1, acceptor2... -> reactor1, ... reactorn -> state representation -> next action
I typically create a SAM instance for every complex form and only communicate high level events to the main SAM instance (submit, ...)
All the form error handling, searches, ... will be handled by the child instance.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:18
When you use a typescript component to implement such complex form, you don't need a complex SAM wiring. You can create a SAM instance, directly inside a TypeScript component. That's what I do with Angular.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:24
I still find big benefits in centralizing mutations in the model.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:41
I do all the API calls in the parent SAM instance though.
Dmitry
@agDmitry
Jan 29 2018 14:55
I found this pattern somewhat interesting:
class Counter extends React.Component {
  state: { count: 0 };
  present ( proposal ) {
    if ( 
      typeof proposal.incrementBy === 'number' &&
      proposal.incrementBy > 0
    ) {
      return { count: this.state.count + proposal.incrementBy };
    }

    // Here we can call setState for another action, but it will be async.
  }
  increment = () => this.setState( 
    ( prevState, props ) => this.present( { incrementBy: 1 } ) 
  );
  // OR
  increment = () => this.setState( () => this.present( { incrementBy: 1 } ) );
  render () {
    return <div>
      Count: {this.state.count}
      <br>
      <button onClick={this.increment}>Increment</button>
    </div>;
  }
}
devin ivy
@devinivy
Jan 29 2018 14:57
wrapping setState() in a react component is very often a good idea... it's the same idea, putting all the code for the critical section in one place.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:57
yes, that's the idea of SAM within a component. It's better than mutating the state directly in the action. I think you will find that's an improvement compared to React/Redux
devin ivy
@devinivy
Jan 29 2018 14:58
then you propose to that wrapped version of setState()
@jdubray what are the reactorNs you reference above?
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:58
but I would still keep a parent instance
devin ivy
@devinivy
Jan 29 2018 14:59
nap()s?
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:59
reactors are just computing the state representation
devin ivy
@devinivy
Jan 29 2018 14:59
ah, gotchya!
Jean-Jacques Dubray
@jdubray
Jan 29 2018 14:59
just to keep the building blocks of the pattern consistent.
Again, just to keep these computed values outside the mutation, because they are not "mutations" per se, though that could be debated.
@agDmitry you should also explore how you will wire API calls.
devin ivy
@devinivy
Jan 29 2018 15:11
@jdubray i think it's worth noting that if you need further separation with meiosis, update() and the model can have a different contract that doesn't have to involving passing the whole model. is that right @foxdonut ?
Dmitry
@agDmitry
Jan 29 2018 15:19

you should also explore how you will wire API calls.

In actions?

Jean-Jacques Dubray
@jdubray
Jan 29 2018 15:21
@agDmitry I usually call queries in actions, and commands (update, delete, create) in the model. But this is not a hard rule. It's more about non-blocking/blocking UX.
devin ivy
@devinivy
Jan 29 2018 15:24
i've been reading about decentralized protocols, and just wanted to mention that i think SAM and meiosis are great fits for writing decentralized applications! https://www.scuttlebutt.nz/
here's an example app https://github.com/Happy0/ssb-chess
Jean-Jacques Dubray
@jdubray
Jan 29 2018 15:40
@devinivy SAM maps to Paxos (propose,accept,learn), so its rather simple to write a consencus-based implementation. Otherwise, yes, each node in the distributed system has a SAM entry point, any node can query the current state representation of that node, at any time. I would use something like SAM-SAFE in distributed scenarios though.
Fred Daoud
@foxdonut
Jan 29 2018 15:40
@devinivy yes that is correct! :thumbsup:
devin ivy
@devinivy
Jan 29 2018 15:42
secure scuttlebutt is append-only, so in that case it's more about creating materialized views of a series of events, married with perhaps some actions taken locally.
it naturally works with streams (one day i hope i'll write a pull-stream example for meiosis!) and fits great into the paxos model.
it's a good fit– and i don't think there's much of a precedent set in that space how to write the applications.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 15:44

@devinivy that's rather trivial for SAM:

creating materialized views of a series of events, married with perhaps some actions taken locally.

Dmitry
@agDmitry
Jan 29 2018 15:48
What about initial creation of model? What if model has to make some request on its creation?
Jean-Jacques Dubray
@jdubray
Jan 29 2018 15:51
I usually do that outside the SAM instantiation. I use a trick in my samples where the initial model looks like a regular JS file, but it's actually served by node.js which makes all these calls. During dev, you can use a static file if it's simpler.
@agDmitry in this sample mode.js is served up by node.js after making some API calls
          // initial model, from file
          // import {data}           from './components/model.data.js'
          // initial model, from server
          import {data}                       from '../app/v1/model.js'
Fred Daoud
@foxdonut
Jan 29 2018 16:17
@agDmitry with Meiosis you can simply make the request to get your initial model, then use it to set up Meiosis. You can also have an empty model initially, and then populate the model with the data obtained from the server. It depends on your goals -- for example whether it is important to show something on the page to the user as fast as possible.
devin ivy
@devinivy
Jan 29 2018 18:25
how do folks deal with writing functional (stateless) components, when a child of your functional component is stateful?
Jean-Jacques Dubray
@jdubray
Jan 29 2018 18:28
that's what I am working on, how do you "fold" component state in the global state in standard way. I got busy and have not finalized it, but the architecture is in place.
The folded state remains there from start to end, whether the component is displayed or not.
devin ivy
@devinivy
Jan 29 2018 18:30
i will be interested to see what you come-up with!
Dmitry
@agDmitry
Jan 29 2018 18:31

but the architecture is in place.

@jdubray can we have a look at it? I'm curious.

devin ivy
@devinivy
Jan 29 2018 18:31
it seems that the child component must just render its default/initial state
Jean-Jacques Dubray
@jdubray
Jan 29 2018 18:36
it's really disconnected
The idea is that the view is still V = f(M)
It just happens that M = { globalState, [comp1, ... compn] }
the component state is passed to every view element and picked up by the corresponding component
Fred Daoud
@foxdonut
Jan 29 2018 18:38
@devinivy would you have a more specific example of a use case for that? writing functional (stateless) components, when a child of your functional component is stateful
Jean-Jacques Dubray
@jdubray
Jan 29 2018 18:38
I think we have gone overboard with "no global state" and "component models".
@devinivy this is what a component definition looks like, nothing out of the ordinary:
var datepicker = function ( id = 'picker' ) {

    const days = 'Su|Mo|Tu|We|Th|Fr|Sa'.split('|')
    const months = 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec'.split('|')

    return {

        id,

        render: function(props) {
            return (html`
            <table id="${id}">
            <tr>
              <td class="btn" onclick="javascript:return go({'${id}': {direction': -1}})">&#9664;</td>
              <td colspan="5">${props.month} ${props.year}</td>
              <td class="btn" onclick="javascript:return go({'${id}': {'direction': 1}})">&#9654;</td>
            </tr>
            <tr>
                ${repeat(props.days, 
                    (i) => i.href, 
                    (day, index) => html`<th id="day-${index+1}">${day}</th>` 
                )}
            </tr>
            ${ props.weeks.map(week =>
              `<tr>
                ${ week.map(day => `<td class=${`btn ${ day.class }`} onclick="javascript:return setValue( {'${id}': { value: ${day.value}}}">${ day.date }</td>` ) }
              </tr>`
            )}
          </table>`)
        },

        actions: [
            { 
                name: "go",
                implementation: function(data, present, model) {
                    data = data || { direction : 1 } ;
                    present(data) ;
                    return false ;
                }
            },

            { 
                name: "setValue",
                implementation: function(data, present, model) {
                    present(data) 
                    return false ;
                }
            }
        ],

        acceptors: [

            { 
                name: "update" + id,
                order: 0,
                acceptor: function(model, data) {
                    if (data[id] && data[id].direction) {
                        model.components[id].offset += data[id].direction
                    }

                    if (data[id] && data[id].value) {
                        model.components[id].value = data[id].value
                        model.components[id].offset = 0
                    }
                }
            }

        ],

        reactors: [
            {
                name: "props",
                reactor: function(model) {
                    let components = model.components
                    components[id].date = new Date(components[id].value)
                    components[id].date.setMonth(components[id].date.getMonth() + components[id].offset)
                     ...
           }
Though you will note that stateful components are instantiated with a unique id and they refer to a specific part of the model. Proposals are also component specific.
This is how such components will be embeded in a stateless component/containe:
<div class="container">
            <div class="row">
                <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
                    ${unsafeHTML(params.description)}
                </div>
            </div>
            <div class="row">
                <div class="col-lg-4 col-lg-offset-2 col-md-5 col-md-offset-1">
                    ${renderers['datepicker1'](params.components['datepicker1'])}
                </div>
                <div class="col-lg-4 col-lg-offset-2 col-md-5 col-md-offset-1">
                    ${renderers['datepicker2'](params.components['datepicker2'])}
                </div>
            </div>
        </div>
devin ivy
@devinivy
Jan 29 2018 18:44
@foxdonut imagine having a functional component for rendering the header of your site, and it contains a dropdown component that has some site navigation.
then one day you decide that the dropdown component needs to show a welcome message for the logged-in user
Hi, Fred!
now you must plumb state down into the dropdown component somehow. is there a way to keep the header component functional, or do you need to use it to plumb state down into the dropdown component?
Jean-Jacques Dubray
@jdubray
Jan 29 2018 18:47
@devinivy I would argue that's more of an application state example. In the case of the date picker, you have the current offset that was chosen last.
I got lost in lit-html, I think I will move the sample to vanilla.js to complete it. (I needed a repeat of repeat and I was not sure how to do that in lit)
Dmitry
@agDmitry
Jan 29 2018 18:49
In React stateful components is not a problem, because they contain very specific state and logic related to that state. Problem is that there is no easy way to parent component to get that state.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 18:50
@agDmitry yes, that's why I am trying to fold it into the global state in a tidy way. So that it can still be accessible if you ever need to, or mutate it from outside the component.
Again, I believe OOP / Component based model and "encapsulation" may be worse than the problem they are trying to solve.
I think we all get it by now, one cannot mutate the global state randomly, that is the problem. Fragmentation + Encapsulation is the wrong solution.
Fred Daoud
@foxdonut
Jan 29 2018 18:51
(I needed a repeat of repeat and I was not sure how to do that in lit)
This is why I dislike templates! Using JavaScript that is never a problem! It's just a nested loop.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 18:52
you are preaching to the choir
But people don't like it when I use vanilla.js
Fred Daoud
@foxdonut
Jan 29 2018 18:53
@devinivy I would pass the state down to the dropdown component, but the header would not need to know any details about it.
Dmitry
@agDmitry
Jan 29 2018 18:57
@jdubray i actually had one big deeply nested immutable state tree in one version of my app. All was fine except for side effects handling. State tree was transformed by pure functions. Another problem is when you pass this tree to a React app, even when all components is optimised, you get poor performance, because, more deeply nested the tree more work needed to be done by React. And i talk about 9-10 levels.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 19:01
@agDmitry That's the role of the State function in SAM it will prune the model into props. Though I am not sure how this will render work with React
Now, it's not a big problem to keep the component state on the side, what's important is that it is accessible to the Model to when mutating the application state.
Dmitry
@agDmitry
Jan 29 2018 19:02

@jdubray

But people don't like it when I use vanilla.js

It might be same people that scream on every corner that jQuery is dead.

Jean-Jacques Dubray
@jdubray
Jan 29 2018 19:03
I don't use jQuery either! only for dedicated components like Datepickers, ...
devin ivy
@devinivy
Jan 29 2018 19:03
@foxdonut that's what i currently do
Jean-Jacques Dubray
@jdubray
Jan 29 2018 19:03
DOM manipulations are dead for sure
devin ivy
@devinivy
Jan 29 2018 19:03
but i would like to be able to render the header without any application state
Jean-Jacques Dubray
@jdubray
Jan 29 2018 19:04
could you qualify "without any application state"
devin ivy
@devinivy
Jan 29 2018 19:06
does the example above help demonstrate?
there's a logged-in user who has a name, and in the application that name displays in the child component of what's otherwise a purely functional component.
Jean-Jacques Dubray
@jdubray
Jan 29 2018 19:08
I am afraid the state have to come from somewhere so when you render the stateless component you do something like this:
${renderers['datepicker1'](params.components['datepicker1'])}
Here it is passed down, but in a generic way, the parent component doesn't know about it
I guess you could create a closure so that you would have only ${renderers['datepicker1']} since the component state is private, it can be prewired to the render method.
devin ivy
@devinivy
Jan 29 2018 19:15
i see what you're getting at
Jean-Jacques Dubray
@jdubray
Jan 29 2018 19:18
it's all about keeping S.A.M in order, it works even with stateful components.
Fred Daoud
@foxdonut
Jan 29 2018 19:33
@devinivy I think I understand a little better. Personally I wouldn't think it's a problem if the header receiving the state and passes it down to the dropdown, even if the header doesn't need to use the state. I don't see it as a "dependency", because you could say that all views are uniform as view = function(state). Does that make sense?
devin ivy
@devinivy
Jan 29 2018 19:35
yes, that is one solution i'm open to, and perhaps the simplest. i just don't love that it ends in passing all state from the root app component downwards.
i do like being able to "sneak" state in to the app, and keep the computation of the app-state internal to the stateful component.
but it might just be too tricky
Fred Daoud
@foxdonut
Jan 29 2018 19:37
well, it doesn't have to be all state everywhere, it could be sub-parts of the state.. but I know that you know that already :smile:
devin ivy
@devinivy
Jan 29 2018 19:39
yes, yes! i imagine i'm probably overthinking it at this point :P
my issue is all about colocation of computing state representation near the components that use it.
Fred Daoud
@foxdonut
Jan 29 2018 19:40
It's possible! Believe me I did a lot of overthinking before arriving to simpler solutions!
Janne Siera
@jannesiera
Jan 29 2018 19:49
Hey guys, thought you might find this interesting: https://www.youtube.com/watch?v=IOiZatlZtGU
devin ivy
@devinivy
Jan 29 2018 19:56
@jannesiera definitely catches my eye– thanks
Jean-Jacques Dubray
@jdubray
Jan 29 2018 20:20
In another life I want to be a mathematician, sigh.
Dmitry
@agDmitry
Jan 29 2018 21:00
@jannesiera thanks!