Skip to content

Instantly share code, notes, and snippets.

@mfeineis
Created March 11, 2018 01:48
Show Gist options
  • Save mfeineis/14a0364e54be9ae8c146d0429054c0df to your computer and use it in GitHub Desktop.
Save mfeineis/14a0364e54be9ae8c146d0429054c0df to your computer and use it in GitHub Desktop.
A self-contained attempt at explaining The Elm Architecture (TEA) in terms of Redux - https://ellie-app.com/kKcXHF8fJa1/0
port module FromReduxToElm exposing (main)
import Html exposing (Html)
port sayHello : (String -> msg) -> Sub msg
type alias Model =
{ who : String
}
type Msg
= SayHello String
main : Program Never Model Msg
main =
Html.program
{ init = ( { who = "Stranger" }, Cmd.none )
, subscriptions = subscriptions
, update = update
, view = view
}
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ sayHello SayHello
]
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SayHello who ->
( { model | who = who }, Cmd.none )
view : Model -> Html msg
view { who } =
Html.text ("Elm: Hello, " ++ who ++ "!")
<!DOCTYPE html>
<html>
<head>
<title>From Redux to Elm</title>
</head>
<body>
<div id="elm-root"></div>
<div id="redux-root"></div>
<div id="earlgrey-root"></div>
<script>
// # Not-that-far-from-actual-Redux setup
// Redux in a nutshell
const Reduxy = {
createStore: (reducer, initialState) => {
const fns = [];
let state = initialState;
return {
dispatch: action => {
state = reducer(state, action);
fns.forEach(fn => fn(action));
},
getState: () => state,
subscribe: fn => fns.push(fn),
};
},
};
// Assuming Flux-Standard-Actions
const reducer = (state = { who: "Stranger" }, action) => {
switch (action.type) {
case "SAY_HELLO":
return {
who: action.payload,
};
default:
return state;
}
};
const store = Reduxy.createStore(reducer);
// The laziest DOM renderer ever :-)
store.subscribe(action => {
const { who } = store.getState();
document.getElementById("redux-root").innerHTML =
`Redux: Hello ${who}!`;
});
store.dispatch({ type: "SAY_HELLO", payload: "World" });
// # Elm setup
const app = Elm.FromReduxToElm.embed(document.getElementById("elm-root"));
app.ports.sayHello.send("World");
// # TEA in JS
// This is our own silly The Elm Architecture 0.18
// implementation using Flux-Standard-Actions as messages
// and `innerHTML` instead of using Virtual DOM for being
// able to compare with Redux. Note also that we're
// hand-waving effect handling, ports and subscriptions
// here to keep the example somewhat succinct. Also be
// aware that this code is hacked together to illustrate
// how TEA might be doing it's magic under the hood.
const Earlgrey = {
Cmd: {
none: Symbol("Earlgrey.Cmd.none"),
},
Sub: {
batch: (...subs) => {
// In lack of a suitable type system we might just
// do it like `redux-saga` and tag our `Sub`s for
// later reference
subs.forEach(sub => {
sub["@@earlgrey-sub"] = true;
});
return subs;
},
none: Symbol("Earlgrey.Sub.none"),
},
// This would be part of the core libraries. Most of the
// usual functionality you'd expect already has a named
// function readily available but if you need something
// specific, like decoding a custom event, there are
// `...withOptions` versions you can configure, i.e.
// with JSON decoders.
Mouse: {
// Contains magic that captures, buffers and handles events
// related to the user's mouse.
downs: constructor => {
// Flows all the buffered events into TEA using the
// the user supplied constructor function
//eventBuffer.map(position => constructor(position));
//moreMagicCleanup();
},
},
//beginnerProgram: ...
program: ({ init, ports, subscriptions, update, view }) => {
// Initial state
let root = null;
let queue = [];
let model = init[0];
let intialCmd = init[1];
// This might be a tad simplified :-)
const render = () => {
const vdom = view(model);
root.innerHTML = String(vdom);
};
// Here the command would trigger some kind
// of effect that eventually put something
// into the queue and schedule render slices
// accordingly. We just mock this by pretending
// the effect happended and put something into
// the queue ourselves
const maybePushCmd = cmd => {
if (cmd && (cmd !== Earlgrey.Cmd.none)) {
queue.push(cmd);
}
};
const maybePushSub = sub => {
if (sub && (sub !== Earlgrey.Sub.none)) {
queue.push(sub);
}
};
// Reducing our message queue using `update` to
// obtain the latest snapshot of the appplication
// state a.k.a. the `model` in Elm
const reduceQueue = () => {
const newCmds = queue.map(msg => {
const [newModel, cmd] = update(msg, model);
model = newModel;
return cmd;
}).filter(Boolean);
// We're done with this slice of work
queue = [];
// Queuing new commands created from the
// `update` reducer step
newCmds.forEach(maybePushCmd);
// Now that we have our most recent model we
// evaluate our subscriptions and push resulting
// messages into our queue
subscriptions(model).forEach(maybePushSub);
};
// If we receive an initial Cmd, we queue that,
// maybe we query our server on App startup?
maybePushCmd(intialCmd);
return {
embed: domNode => {
root = domNode;
const thisPortSetup = {};
// Creating ports from our `actionCreator`
// look-a-likes
Object.keys(ports).forEach(portName => {
thisPortSetup[portName] = {
send: payload => {
queue.push(ports[portName](payload));
reduceQueue();
Earlgrey.schedule(render);
},
//subscribe: fn => ...
};
});
// Scheduling the initial render
Earlgrey.schedule(render);
return {
ports: thisPortSetup,
};
},
//fullscreen: ...
//worker: ...
};
},
// Async scheduler with `setTimeout` fallback
schedule: window.requestAnimationFrame
? fn => window.requestAnimationFrame(fn)
: fn => window.setTimeout(fn),
};
// The constructor functions for `Sub`s are really just `actionCreator`s
const subs = {
letsHearEngage: () => ({ type: "LETS_HEAR_ENGAGE" }),
captureMouse: position => ({ type: "MOUSE_DOWN", payload: position }),
};
// The actual program in terms of TEA. It only consists of constants
// and pure functions, Elm can enforce that with its type system, in
// JS you need to rely on conventions...
//
// Note that Redux took inspiration from Elm so it's
// actually not so strange that the setup seems rather
// familiar.
const Hot = Earlgrey.program({
// Provide the initial model and maybe an initial command
// for the runtime to evaluate when the app is loaded
init: [{ who: "Stranger" }, Earlgrey.Cmd.none],
// We're hand-waving the setup but you can tell that
// ports work a lot like `actionCreator`s
ports: {
sayHello: payload => ({
type: "SAY_HELLO",
payload,
}),
},
// This isn't working in our mock setup but this tells TEA
// what outside signals it should subscribe to
subscriptions: model => Earlgrey.Sub.batch([
// You can put `Sub msg`s into the queue based on the current
// model. JS is totally fine with dispatching stuff our program
// doesn't know about, Elm makes sure that this doesn't happen
model.who === "Picard" ? subs.letsHearEngage() : Earlgrey.Sub.none,
// Interactions with the outside world can be channeled
// into TEA via `Sub`s too, like `mousedown` events
Earlgrey.Mouse.downs(subs.captureMouse),
]),
// Like Redux TEA uses a *pure* reducer function called `update`
update: (msg, model) => {
switch (msg.type) {
case "SAY_HELLO":
return [{ who: msg.payload }, Earlgrey.Cmd.none];
default:
return [model, Earlgrey.Cmd.none];
}
},
// No VDOM but you get the point: the view is just a *pure*
// function from `Model` to a description of the view
// you want TEA to display. If you're using `Platform.program`
// there is no `view` and the only way to create an app
// from this program is by calling `App.worker()`
view: ({ who }) => `Earlgrey: Hello ${who}!`,
});
// Go!
const tea = Hot.embed(document.getElementById("earlgrey-root"));
tea.ports.sayHello.send("World");
</script>
</body>
</html>

MIT License

Copyright (c) 2018 Martin Feineis

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment