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