Martin Janiczek
2017-11-02 00:09:55 UTC
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 }
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.
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.