One of the big questions we see these days is “how do I use the Elm Architecture with tasks?”
So the goal here is to outline how to have components nested deep in your application that can trigger effects.
So the overall picture is that we have a pure and immutable chunk of code called “the Elm Architecture” where we describe our whole application. We then have tiny services on the borders where all of our effects are run and then routed back into our application.
A number of nice benefits come from this pattern. They show up at least in the case of HTTP communitation and database communication.
-
HTTP: Let's say you are loading a bunch of information, possibly redundant in various ways. In the normal world, every component sends a bunch of requests independent of everything else, so if 10 things need your profile picture, they all send that request. By routing all the HTTP effects to a centlized place, we can ensure that these are batched, perhaps making things much faster.
-
Databases: Let's say a bunch of components generate changes they'd like to send to the database. It is concievable that you want to turn a bunch of haphazard changes into a single transaction that will succeed or fail in a way that is easy to understand and work with. By batching all the database effects in one place, there can be some logic about how to batch things into transactions, or perhaps avoid sending the requests altogether under certain circumstances.
While it is nice that you actually can get some nice things from this setup, it really is not a choice for us. Ultimately, we have to do things this way if we want to continue to manage effects. So the question is effectively, how do make this happen in a regular and easy to understand way.
This essentially extends the basic Elm Architecture with some way of threading effects around.
-- BASIC STUFF
model : Model
view : Address Action -> Model -> Html
update : Address Action -> Action -> Model -> (Model, Effects)
-- We get an address in so we know how to route any effects
-- EFFECTS
type Effects
-- An opaque type, conceptually a list of tasks.
-- It can be constructed in very specific ways.
doNothing : Effects
arbitraryTask : Address action -> Task Never action -> Effects
-- When the `Task` is done, the result is routed to the given `Address`.
-- `Never` is an uninhabited type, no value exists that has that type.
-- See http://package.elm-lang.org/packages/imeckler/empty/latest
-- The point is that we can specify "this task must not fail" which is
-- another way of saying "you must explicitly handle any potential errors"
http : Address action -> Http Never action -> Effects
-- `Http` is some custom thing like `Task`, but with extra restrictions
-- By making it its own thing, we can analyze it to automatically do any
-- necessary batching and caching.
database : Address action -> Database Never action -> Effects
-- `Database` is the same idea as `Http` where we can have some custom
-- chainable thing that can be turned into atomic transactions in a nice way
batch : List Effects -> Effects
-- Sometimes you want to do a batch of things all at once.
-- START
start : App model action -> Output
type alias App model action =
{
model : model,
view : Address action -> model -> Html,
update : Address action -> action -> model -> (model, Effects)
}
type alias Output =
{
frames : Signal Html,
-- hand this to main
effects : Signal (Task () ())
-- hand this directly to a port, it will run effects and route the results
}
Now I think the two main concerns here are:
- Will it be a pain to manually route
Effects
? If I am updating the subcomponents, I need to be sure to gather all their effects together and say something likebatch subeffects
in myupdate
function. - It seems like the ideal version of this will allow people to write custom effect managers. If you want to integrate with IndexDB or MySQL or whatever, maybe you want to write a custom thing just for that. I think this is doable, but I don't have enough experience yet to be able to write down what that'll look like.
I have been starting to organize effects in a slightly different way, which I thought I would describe here in case it was interesting.
The intuition that I started with was that it was slightly awkward to have the
update
function return a tuple. I'm not sure exactly why I have that intuition -- perhaps it is that the update function now seems to be doing two jobs. One job is to update the model. The other is to generate a task (or tasks). I wondered what it would look like to separate those two jobs.So, I started creating a new function (at each level of a component hierarchy) that looks a little like this:
The name
reaction
is based on this intuition: some actions result not just in model updates, but in a chain-reaction of further work (here, tasks).Now, how does this get hooked up? At the top of the component hierarchy, I've got something like this. (Note that all the code here is somewhat modified from what I actually use, which is split up a little differently and involves merging multiple Signals from different modules etc., so there may be typos etc., but you should get the idea).
So, basically, what I've done is have 2 signals separately listening to the actions signal. One handles the normal foldp -> model update work. The other handles generating any tasks which need to be executed. (I say 'tasks' because it could be a
sequence
if needed, and theandThen
andonError
etc. may compose a bunch of tasks if needed.)If you compare this with the structure you've outlined, one of the differences is that my
reaction
method only has read access to the model. For writing, the reaction method simply generates tasks which, in the appropriateandThen
oronError
, send a message which will update the model eventually. (Originally, I hadn't even letreaction
have read access to the model, but eventually I came across a case where it was awkward to provide all the needed information in the action itself).This means that I often deal with an Action both in my
update
method and myreaction
method. For instance, consider my LoginUI module. It has an Action which looks likeAttemptLogin Credentials
. Whenupdate
sees that Action, it makes a little update to the model to note that a login attempt is in progress (for instance, the view method might want to display something to note that). Whenreaction
sees that Action, it returns aTask
which actually will attempt the login (i.e. make the http request).Now, it would certainly seem comprehensible to me that someone might have the intuition that it's odd to do these two things in two different functions. But there is something that I personally like about it, though I do find it a little hard to articulate exactly why.
The other thing I should mention is that I have been "centralizing" the construction of the tasks in modules, mostly to separate out UI concerns from other kinds of concerns. Consider the Login UI again (I've just been working on that, so it's top of mind). I actually have an
AccountService
module, among whose functions is something like this:I'll leave out the implementation, but basically it uses elm-http to construct the http request, and then uses
andThen
andonError
to do two things:-- Massage the errors and results into more useful types (within the logic of my program)
-- Send a message which will update the model to show who is logged in.
Note that the LoginUI module doesn't need to worry about doing the latter thing -- the task it gets back will ensure that it is done. But, the LoginUI module can, of course, add its own
andThen
or whatever in order to do some UI-related work. For instance, the LoginUI model might want to use anandThen
to send a message that will update the model in a way that therender
method will move to a different 'page' (that is, a different virtual page from the UI logic's point of view).My point is that once you're in Task-world, composing things together like this is actually pretty easy -- you can divide up the work between modules in a sensible way without leaving Task-world. You only really need to leave Task-world when you need to update the model -- that is, by sending a message -- but, of course, sending a message is itself a Task, so it's easy to integrate into the various
andThen
s or sequences or whatever shape the combined task actually has.Anyway, I hope this makes some kind of sense.
Just to provide some background, I'm working on porting what was originally a JHipster app (i.e. Angular + Spring) to Elm, and having a lot of fun doing it. The source code is now available at https://github.com/rgrempel/csrs-elm