Skip to content

Instantly share code, notes, and snippets.

@evancz
Last active May 21, 2017 05:01
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evancz/44c90ac34f46c63a27ae to your computer and use it in GitHub Desktop.
Save evancz/44c90ac34f46c63a27ae to your computer and use it in GitHub Desktop.

Components with Effects

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.

Overall Picture

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.

services

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.

Basic Version of an Effectful Architecture

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:

  1. 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 like batch subeffectsin my update function.
  2. 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.
@mgold
Copy link

mgold commented Jul 13, 2015

start : App model action -> Output -> Shouldn't model and action be types, not type variables? Also, the tuple in update makes it tricky to implement. I think this would work?

start {model, view, update} =
   let (models, effects) = Signal.foldp (\a m -> update anAddress a (m, DoNothing)) model actionsSignal
       views = Signal.map (view anAddress) models
   in Output views effects

@rtfeldman
Copy link

Some thoughts:

  1. The Output part seems nice and clean.
  2. Effects makes good sense to me as an API, as doing it that way now can have nice benefits in the future.
  3. I presume the idea is that if you need to "loop back," the Effect's Task sends an Action to the provided Address?
  4. Is it maybe better to have update use batch by default? That way instead of returning (model, Effects.doNothing) you can just do (model, []) which is more concise.

I like where this is going!

@rtfeldman
Copy link

Another thing that comes up with nested components is that they sometimes need to send to ports.

Is it conceivable that Effects could be responsible for that too?

@rtfeldman
Copy link

As far as it being a pain to manually route effects, batching effects for subcomponents sounds like less of a pain than what we currently do for translating to/from parent/child models, so I'm not worried about that. 😄

@rgrempel
Copy link

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:

reaction : Address Action -> Action -> Model -> Maybe (Task () ())

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).

actions : Mailbox Action
actions = mailbox NoOp

models : Signal Model
models = foldp update initialModel actions

tasks : Signal (Maybe (Task () ()))
tasks = Signal.map2 reaction (.signal actions) models

port execute : Signal (Task () ())
port execute = Signal.Extra.filter (succeed ()) tasks

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 the andThen and onError 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 appropriate andThen or onError, send a message which will update the model eventually. (Originally, I hadn't even let reaction 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 my reaction method. For instance, consider my LoginUI module. It has an Action which looks like AttemptLogin Credentials. When update 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). When reaction sees that Action, it returns a Task 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:

attemptLogin : Credentials -> Task LoginError (Maybe User) 

I'll leave out the implementation, but basically it uses elm-http to construct the http request, and then uses andThen and onError 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 an andThen to send a message that will update the model in a way that the render 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 andThens 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

@rtfeldman
Copy link

For what it's worth, I also feel it's a bit awkward to return a tuple...I'm curious what wouldn't work about this:

update : Address action -> action -> model -> Task error model

I'm assuming there's something lame about it; I just don't know what.

@rtfeldman
Copy link

Oh wait, I think I just figured it out...that implementation is prone to race conditions. Like if you start with model1 and kick off an HTTP task, then somebody else comes along with model2 and kicks off another HTTP task, but the latter finishes first, then the former will finish and happily extend model1 to determine the new model, overriding whatever impact on model the latter wanted to have.

Making sure all the Tasks do is generate more Actions not only avoids this problem, but makes sure it can't happen.

tl;dr I'm dumb and this is better.

@mgold
Copy link

mgold commented Jul 14, 2015

@rgrempel I really like that example and I agree with your intuition. I think reaction is a great name. However, I think that sometimes you will need the model. For example, I have a text field and a submit button. Because of how Grahics.Input.Form works, the field content is stored in the model. To send it to the server when the submit happens, I need access to the model. I think that a well-placed sampleOn could ensure that the model is updated (and saved in the typical foldp way), and then passed with the same action to reaction to see if anything effectful needs to happen.

@mgold
Copy link

mgold commented Jul 14, 2015

This just came up on the list, where the model informs the reaction, and the original action is not (directly) needed. So I think any approach is going to need to create a reaction based on both. Cruddy ASCII signal flow diagram:

old model ----new model---\
action-------/-------------reaction

@aphorisme
Copy link

After some days off, I had another look into my question which Max pointed out to be related to this gist.

After all, I've came up with a general idea I put into a library; maybe it can give you some inspiration on your thoughts: (https://github.com/aphorisme/elm-oprocesso)
It's very fresh, but I will use it on my next apps, so it might evolve into something helpful.

@aphorisme
Copy link

Just in case you already had a look: I've decided to change the main type, such that one can glue actions together. I'm on the run for now, but I will finish it tomorrow (or later).

@rgrempel
Copy link

@mgold I did indeed eventually have to supply the model to the reaction function -- as you predicted, I came across a use case that needed it.

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