Skip to content

Instantly share code, notes, and snippets.

@evancz
Last active April 15, 2018 20:42
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save evancz/e4e4173382a9024f95d1 to your computer and use it in GitHub Desktop.
Save evancz/e4e4173382a9024f95d1 to your computer and use it in GitHub Desktop.

There has been some discussion of how to make "custom" effects. The elm-effects library can express anything, but there are cases where you may want to do some custom analysis befor triggering a bunch of tasks.

This gist describes how we would extend elm-effects with a module called Effects.Custom that would let users "define their own effect interpreters" that do more clever things.

Basic Idea

The core technique is to make the Effect type have a hole for whatever you want.

module Effects.Custom where

type Effects fx msg = ...

map : (fx -> fx') -> (msg -> msg') -> Effects fx msg -> Effects fx' msg'

tick : (Time -> msg) -> Effects fx msg

task : Task Never msg -> Effects fx msg

custom : fx -> Effects fx msg

batch : List (Effects fx msg) -> Effects fx msg

-- StartApp.start : (List fx -> Task Never msg) -> ...

So you fill in the fx hole with the custom thing.

Rough GraphQL Example

Whenever you define a custom Effects type, the fx argument should have msg type in it. So imagining a GraphQL API, you would have something like this:

module Effects.GraphQL where

import Effects.Custom as Fx

type alias Effects msg =
    Fx.Effects (GraphQL msg) msg

type GraphQL msg = ...

query : GraphQL msg -> Effects msg

graphMap : (msg -> msg') -> GraphQL msg -> GraphQL msg'

map : (msg -> msg') -> Effects msg -> Effects msg'
map tagger myEffect =
  Fx.map (GraphQL.map tagger) tagger myEffect
  
interpreter : Address msg -> List (GraphQL msg) -> Task Never ()

The point here is that we can have the GraphQL type be all data, no functions. So when we run the interpreter function on everything we can combine queries to the best of our abilities.

It may be necessary to have an "initialization phase" for any effect interpreter. I can imagine you may want to do batching across frames, so maybe you want a 400ms delay before you dispatch things so you can do more batching. In that case, we'd need to get a bit fancier about the type of interpreter but this also seems relatively easy to do. This would mean we have fleshed out our concurrency stuff a bit more though.

Combining Custom Effects

Folks will inevitably want to put together Effects.GraphQL and Effects.FireBase or whatever it happens to be. You can do this with roughly the same technique as used in the previous section.

module Effects.FireGraph where

import Effects.Custom as Fx
import Effects.GraphQL as GraphQL
import Effects.FireBase as FireBase

type alias Effects msg =
    Fx.Effects (FireGraph msg) msg

type FireGraph msg
    = GraphQL (GraphQL.GraphQL msg)
    | FireBase (FireBase.FireBase msg)


fgMap : (msg -> msg') -> FireGraph msg -> FireGraph msg'
fgMap tagger fg =
  case fg of
    GraphQL gq ->
        GraphQL.graphMap tagger gq

    FireBase fbq ->
        FireBase.fireMap tagger fbq
        
        
map : (msg -> msg') -> Effects msg -> Effects msg'
map tagger myEffect =
  Fx.map (fgMap tagger) tagger myEffect
  
  
interpreter : Address msg -> List (FireGraph msg) -> Task Never ()
interpreter address requests =
  let
    (fireRequests, graphRequests) =
      partition requests
  in
    Task.map2 (\_ _ -> Task.succeed ())
      (GraphQL.interpreter address graphRequests)
      (GraphQL.interpreter address fireRequests)

It is not clear to me what best practices would be around code bases that use multiple effect interpreters. I think we'll find out as we go.

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