So to report back on how this went - it probably isn’t such a useful pattern because it doesn’t scale.
I started out by forming the Day convolution using joneshf/elm-comonad from its Html.Alternative module. Then I rewrote that as a combinator of more normal Elm Programs with init/model/update/subscriptions and a Msg type (joneshf/elm-comonad does away with the Msg type).
I introduced also the concept of a message channel between the 2 combined programs, which is very similar to an out message. The difference is that the 2 combined programs are not combined in a parent-child nested TEA pattern, but are paired together side by side, to form a new combined program.
Here is the code:
So I could use this to write my authentication module which has its own private internal state, as a HeadlessProgramWithChannel. Then I could write a user interface program as an HtmlProgramWithChannel. The messages going from the UI to the auth module would be a Msg type describing the actions it is requested to take (login/logout/unauthed). The messages coming back from the auth module would be a record type describing just a portion of the total authentication state - a status saying whether we are currently logged in or not, and if logged in what the user id and permission scopes are. The JWT token in particular would not be visible at all on the UI side, and there is other internal state too, such as a record of when the token expires, and a refresh token to obtain a new one, and so on.
The problem is that the combined program type is a product of the types of the programs it combines:
combineHeadlessAndHtmlWithChannel :
HeadlessProgramWithChannel modela msga send recv
-> HtmlProgramWithChannel modelb msgb recv send
-> HtmlProgram ( modela, modelb ) (Msg msga msgb)
The only way I can think of to loosen the typing is to convert all messages and models to Json.Encode.Values. Then it would be possible to combine many programs in this way, as a kind of plug and play message bus, but at the cost of writing a lot of encoders/decoders.
It is interesting to compare with an OO language, where sub-typing allows types to be loosened. So in an OO language I could have a list of plugins (List Plugin) where each Plugin defines an interface and has its own implementing sub-type. At the level where the plugins are combined into a system, the typing is loosened sufficiently that they are all just plugins with a defined mechanism to plug them together. In Elm typing is much stricter, which rules this out, except by the mechanism of converting to Value. Its like trying to put a Plugin a and Plugin b into the same list in Elm; it won’t let you do it unless a and b are the same type. Or by having common global message and model types across the whole application which may defeat the purpose, and not all modules need to talk to each other.
I only have one auth module with state, all my other modules that talk to back-end APIs are stateless. So I could use the above combinator to integrate the auth module, and save myself the tiny hassle of adding a clause to the parent Msg type using Cmd.map to link in its update cycle to the application.
Is it worth it? Probably not, not without gaining something in terms of easier scalability.