Discussion:
[elm-discuss] Multiple "Middleware" pattern instead of single *.program
Martin Janiczek
2017-11-02 00:09:55 UTC
Permalink
Hello elm-discuss!

I've been thinking about the **.program* pattern. Currently one has to
choose one of the many different *.program functions
<http://klaftertief.github.io/elm-search/?q=program> and implement the rest
of the functionality themselves, ie. they can't mix and match, say,
*Navigation.program* and *TimeTravel.Html.program*.

A way to solve that would be to specify a list of "middlewares" that each
do a specific task, and have a "clean" program they all augment:

main =
combineMiddleware
Navigation.middleware
TimeTravel.middleware
DropboxAuth.middleware
App.program

I have tried to implement such a pattern and want to share it and ask for
opinions. *This could, after all, be a very bad idea!*

----

Some links:

- GitHub <https://github.com/Janiczek/middleware> (I didn't want to
publish that as a package just yet, but if it helps somebody, tell me and I
can do that)
- Tour of the code:
- The *main* function
<https://github.com/Janiczek/middleware/blob/master/src/Main.elm>
- The *program* (business logic)
<https://github.com/Janiczek/middleware/blob/master/src/ExampleProgram.elm>
(a counter)
- A *middleware* "talking" to the program
<https://github.com/Janiczek/middleware/blob/master/src/Middleware/ResetByMsg.elm> (returns
it from update)
- A *middleware* logging "all the Msgs under it"
<https://github.com/Janiczek/middleware/blob/master/src/Middleware/History.elm> (this
+ the Reset middleware could, with a bit of effort, become a Debugger
middleware)
- A *middleware* using its own Subs
<https://github.com/Janiczek/middleware/blob/master/src/Middleware/SubsTest.elm> (the
middlewares can use Subs, Cmds, have their own model)

----

This allows people to combine multiple behaviours instead of being limited
to just one.

*Questions I'm pondering are:*


1. Is this a good idea at all?
2. Comparison to the "fractal TEA" that we generally shun now. (Hiding
of behaviour; in fractal TEA one sees the extra functionality in the model,
here it's almost invisible; does one need to see it? This approach avoids
some boilerplate -- the only thing end user needs to supply is a Msg
constructor, similar to *.program)
3. Does this encourage some bad practices, code smell, OOP in a FP
language, componentization? Good/bad? (If bad, is the *.program pattern we
currently somehow embrace OK then? I'd say it does things /hides behaviour/
basically the same way.)

----

And, for the interested, some implementation details:

- Each middleware knows about the next model in the chain (they're
nested), but can't inspect it. (If it used concrete type instead of a type
variable, it would limit what other middlewares/programs can be next to it.)
- The Msgs are also nested: each middleware has to have one Msg for
wrapping the Msgs of the next middleware/program in the chain.
- All middlewares can send messages *to the program* (but not to each
other):
- the program exposes a record with Msg constructors it offers
alongside update, init, etc.
- each middleware declares what Msg constructors it needs (through an
extensible record) -- very similar to how Navigation.program needs a
Msg constructor for the location changes
<http://package.elm-lang.org/packages/elm-lang/navigation/2.1.0/Navigation#program>
.
- the compiler makes sure all middleware Msg needs are satisfied
- middleware's update gets the record with the constructors as an
argument, and returns a Maybe Program.Msg
- the Msg gets threaded through the Elm Runtime as any other, and the
user gets a nice clean Msg in their update.

And some API:

middleware :
{ init : (innerModel, Cmd innerMsg) -> (ownModel, Cmd ownMsg)
, update : ownMsg -> ownModel -> programMsgs -> (ownModel, Cmd ownMsg,
Maybe programMsg)
, subscriptions : ownModel -> Sub ownMsg
, view : ownModel -> innerHtml as Html ownMsg -> Html ownMsg
, wrapMsg : innerMsg -> ownMsg
, unwrapMsg : ownMsg -> Maybe innerMsg
}

where

ownModel = { ownFields | innerModel : innerModel }

program :
{ init : (model, Cmd msg)
, update : msg -> model -> (model, Cmd msg)
, subscriptions : model -> Sub msg
, view : model -> Html msg
, programMsgs : programMsgs
}

where

programMsgs = (eg.) { locationChanged : Location -> Msg }
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
'Rupert Smith' via Elm Discuss
2017-11-02 09:30:10 UTC
Permalink
Post by Martin Janiczek
1. Is this a good idea at all?
Having stacked a few programs together by hand, this strikes me as an
excellent idea and one that is worth exploring.

One problem I had was with TimeTravel.program which I was wrapping a
RouteUrl.navigationApp with. All the messages were then nested at the
time-travel level, and its UI did not support nested filtering, so I could
not easily filter out animation messages. It occurs to me that if each
middleware knows about the next layer, and has wrap/unwrap functions for
it, that it would be possible to have set up the time-travel layer to
unwrap the nested messages, if I had such a system as yours.

Would love to see what a navigation example looks like.

Rupert
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Martin Janiczek
2017-11-02 11:56:52 UTC
Permalink
Post by 'Rupert Smith' via Elm Discuss
One problem I had was with TimeTravel.program which I was wrapping a
RouteUrl.navigationApp with. All the messages were then nested at the
time-travel level, and its UI did not support nested filtering, so I could
not easily filter out animation messages. It occurs to me that if each
middleware knows about the next layer, and has wrap/unwrap functions for
it, that it would be possible to have set up the time-travel layer to
unwrap the nested messages, if I had such a system as yours.
Right now that seems problematic: the composing functions can't inspect the
types in runtime (to decide how much to unwrap), and the amount of wrapping
the time-travel middleware will see depends on where you'll put it in the
chain. It would make sense to put it right next to the user program - no
middle layers to unwrap then.
Post by 'Rupert Smith' via Elm Discuss
Would love to see what a navigation example looks like.
I have added a Navigation middleware example, see the updated demo:
https://janiczek.github.io/middleware/index.html
(Because *elm-lang/navigation* doesn't expose the *Location -> msg -> Sub
msg*, I've had to copy it to the user-space code to be able to connect the
subscription to the middleware.)

This brought a minor change to the API: middleware's *subscriptions* now
take the *programMsgs* record and return *(Sub msg, Sub programMsg)*.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Martin Janiczek
2017-11-02 12:17:15 UTC
Permalink
Post by Martin Janiczek
https://janiczek.github.io/middleware/index.html
(Because *elm-lang/navigation* doesn't expose the *Location -> msg -> Sub
msg*, I've had to copy it to the user-space code to be able to connect
the subscription to the middleware.)
(Of course I meant *(Location -> msg) -> Sub msg*.)

It seems to me that the Navigation example doesn't gain much from being
written in middleware pattern. It's *better than the *.program approach* in
that it can compose, but it's *worse than Sub approach* in that user
doesn't see where the Location msg came from. (In my opinion, it's better
to be explicit.)
The time-travel middleware does, I think, still have its value in being in
the middleware pattern: it's more "invasive" change, *view* and all, and
wouldn't be suited by simple Cmd/Sub.

I assume the reason Navigation has opted for the *.program approach, is the
custom *init* function. This (init) is one thing I haven't yet thought
about much for the middleware, and is probably subject to change. There
probably exists some nice middleware example that would make the API more
clear. Food for thought :)
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Loading...