Is TEA a comonad?

I got a little intrigued by the idea of combine ‘programs’ in some meaningful way in Elm.

Suggests that TEA is similar to a comonad. It also suggests that comonad user interfaces would be able to be combined together, such as display 2 at the same time. Well that doesn’t sounds completely useful, but I realize this is just an exploration of ideas, of academic interest.

I don’t know what a comonad is, so I thought I would find out. This slide deck was pretty helpful:

Intuitively its described as a side-effectual computation where a value is extracted in some context. Like the side effects of users selecting actions, updating a model, then displaying a new view in the context of the model. So yeah, it looks more comonadic to me than monadic.

I do use the nested TEA pattern. Yes, I know we try to avoid it and there is too much boilerplate and we don’t like components here, and its better to extract functions on the view or update before having to resort to it. One thing I find pushes me in that direction is animation state - I don’t want the consumer of my piece of UI to have to manage that, so it tends to end up as a stateful thing, like a component, whenever there is animation (another good reason to have not too much animation). Sounds terrible, but I am glad to see that Richards best-practice SPA does it too: https://github.com/rtfeldman/elm-spa-example/blob/master/src/Main.elm#L226

So I am thinking comonads are great when you have higher levels of polymorphism than Elm, by which I mean Haskell. And you can write clever functions that work with them to do clever stuff. But TEA is a concrete thing, so it does not need to be exactly a comonad, although its good to get my head around this stuff.

What useful ways can I combine multiple TEA structured programs together? The obvious one is the parent-child structure. So I am thinking, is there some set of ‘combinators’ that helps me build a nested TEA and takes care of the boilerplate aspect of it?

3 Likes

The other thing I wanted to mention, was taking a look at the joneshf/elm-comonad examples:

https://ellie-app.com/fLBqHbdNLa1/0

Stylistically, this seems bad and is similar to other previous suggestions that we just compute the next model in the view. update/view provide a nice separation of business logic from view logic, and this style loses that. But not to be too critical, I realize its just an experiment.

The essence of what this is asking is, “How can I do object-oriented programming in Elm?” I realize that it’s using different words to ask it, but that’s fundamentally what it’s asking.

I gave a talk on what I learned from going down that road early on in my time with Elm.

I think “how can I scale Elm applications in a nice way?” is the real important question to ask here, and in that link I explained what is by far the best approach I’ve found. :slightly_smiling_face:

Yes, I was there when you gave the talk and it was very good advice. I could ask though, why you didn’t take your own advice and used the parent-child TEA structure in the elm-spa-example?

But yes, the question is how can I scale Elm in nicer ways, and the answer may well be instead of trying to do a parent-child TEA structure, do … instead. I will follow up with a concrete example, as my question is somewhat incoherent, grasping at ideas.

So here is an example where I went parent-child TEA:

The out message can be taken away by instead passing in a function to the view, just like it is done in this favorite button in the elm-spa-example:

My button bar has animation state and logic around that animation. I’d like to prevent the consumer of it having to know about this, which pushed me in the direction of holding internal state in its Model, and also hooking it up to the animation subscriptions. This forced me down the route of using init/update/subscriptions from TEA in this module.

Similarly: https://github.com/rtfeldman/elm-spa-example/blob/master/src/Views/Article/Feed.elm

I’m looking for ways in which I can more easily integrate lifting the update/subscriptions into the parent that consumes this module - by writing some piece of code that handles that for me. Or rethinking it and doing it in a completely different way.

Hm, I don’t think that accurately describes how elm-spa-example is structured. If you’re referring to the fact that I used Html.map and Cmd.map in some places, I talked about the problem those solve, as well as why it’s a mistake to use Model/view/update triplets as the fundamental application building block—which elm-spa-example clearly doesn’t do; only three modules in the whole project call Html.map, and only three modules call Cmd.map.

I don’t think the bolded part will work. Knowing about the animation is fundamental to this API; there are distinct init and initWithAnimation functions, and the caller has to choose which to call! Also, several functions expect the caller to specify the very styles and interpolations the animations will use. This module is so tightly coupled to animations, I don’t think it’s achievable for users not to know about it.

Here’s how I would write this API: See https://discourse.elm-lang.org/t/is-tea-a-comonad/527/5 · GitHub

This version has ~75% fewer lines of code based on some observations I made:

  1. id is never used internally or passed to other modules, so I removed it.
  2. ControlBar never modifies shown or buttons inside update because they are configuration, not state. I removed them from Model.
  3. Without shown, the show and hide functions exist to call animateStyle passing arguments that came directly from the caller, unmodified by ControlBar. So I cut out the middleman by removing show and hide; the caller can pass those arguments along directly to animateStyle without the indirection of asking show and hide to do it.
  4. Similarly, without shown, ControlBar.subscriptions exists to call Animation.subscription passing values ControlBar never modifies, and ControlBar.update exists to call Animation.update passing values ControlBar never modifies. Since the callers already know these arguments, they can pass them along directly to Animation.subscription and Animation.update without the indirection of asking ControlBar to do it.
  5. Without id, shown, and buttons, there’s no longer any state stored other than Animation.State itself. We don’t need a Model record to hold a single value, so we can safely delete Model, init, and initWithAnimation, and just have view accept the current Animation.State along with some config (including the String -> msg function you mentioned earlier in the thread).

In a world where this module wanted more control over its animations, I could see wrapping Animation.State in an opaque type, but this API wants to let callers customize the animation as much or as little as they like, so that doesn’t seem like the right fit here.

To me, the way I’d write it is easier: if you want an animated ControlBar, have update call Animation.update and subscriptions call Animation.subscription directly. If you don’t want animations, pass Nothing to view instead of Just animationState and you’re done!

Hope that helps!

The point of my badly worded question (“why you didn’t take your own advice and used the parent-child TEA structure in the elm-spa-example?”) was not so much to say that you used it as the fundamental building block, but to point out that you did use it in some places. In other words, unless you refactor the code to remove all uses of it, you have to concede that in some cases it should be used?

I know I have used it too much so thanks for taking the time to refactor my code, much appreciated and much that I can learn from that.

I have another package which is quite complex - it handles the authentication side of the application, is re-used across several applications, and it has internal state that it hides from the consumer of it. The internal state is hidden partly for security, to stop consumers of it putting the JWT token in unsafe places, and tries to present the whole authentication piece as a nice API with a simple set of actions you can perform on it (log on, log off, refresh, am I logged on?, what scopes am I authorized for?). It is a pain to integrate with so I am thinking of ways I can re-write it to reduce that. It was much easier to integrate with when I was doing so through elmq (GitHub - rupertlssmith/elmq).

Anyway, I won’t post it up here and expect you tell me whats wrong with it. I’ll take some of the ideas you have given me and see if I can fix it myself, and also have a little experiment with some of the things I alluded to in the OP.

This feels like a little bit of a cheat in the simplification and/or line count since Rupert did have functions that modified shown. He just didn’t use update to access those functions. Once one starts arguing against TEA “triplets”, it feels like a bit of a stretch to use the fact that an update function doesn’t modify something as a reason to remove it. On the other hand, this does point to a clear way that Rupert’s code can be refactored and simplified because it factors into a portion that deals with animated showing and hiding and a part that deals with buttons. The animated show and hide may well justify being in its own module for reuse elsewhere and can be separated from a buttons implementation much more like Richard’s. So, a full refactor should probably break the code into a buttons module (view function) and a show/hide animation model and leave it to the containing code to stitch the two simpler pieces of code together.

All of these could arguably be further simplified by replacing the use of string names for buttons with a message parameter on the button type. Then the configuration step can just supply buttons with icons and messages.

A bigger stylistic question is whether one adds an animation parameter to view functions when the design calls for animation or as a matter of practice. Type-checking makes it easy to add later, but in my experience engineers often become reticent to make changes in code they didn’t write and also to make changes that will require updating all uses. The latter can be mitigated by adding a viewWithAnimation function when we find a need for an animation but the first friction point remains. The correct call probably depends on team dynamics and cross-project code reuse. If people can feel comfortable updating an API both because they are used to it and because the consequences can be limited to the project at hand, then keep things simple while establishing a clear pattern for the more complex case. If you can’t make those guarantees, then it may be better to build for the general case from the beginning.

Turning back to the start of this thread, this trade off goes to some of the questions around any sort of nested use of TEA. When TEA really was triples, it didn’t feel that bad. A bit of boilerplate but it was manageable. But then 0.17 brought subscriptions which are often null but result in code that doesn’t work if you don’t hook them up when they are needed—possibly for non-obvious reasons. So, now we’re collecting and mapping commands in init, collecting and mapping subscriptions, processing nested messages in update, and mapping messages in view. None of it is hard, but programmers balk at boilerplate. And if you need to communicate between parent and child, the boilerplate gets messier still. The type system can be used to make this code reliable, but it still leads to a lot of code. At this point the pendulum swings toward avoiding fractal decomposition and pushing more things into the top-level model because there’s a reason people have been drawn for years to just sticking stuff in global variables which is essentially what tossing everything into one big model is essentially doing.

As someone who reaches for C rather than C++, Elm’s relative lack of sophisticated mechanisms that would reduce boiler plate hasn’t bothered me as much as it does some. Boiler plate feels better than magical pieces of indirection that by not being invisible and dead simple aren’t really all that magical.

Finally, and I think this ties to the login token case, where a components view can definitely lead code astray is in structuring the data model based on the view hierarchy even when that doesn’t make sense. Going flat “fixes” that by removing hierarchy as an issue. Redux fixes that in JavaScript by have one data model store. One could apply the Redux pattern to Elm using something like the Elm taco pattern or if flat by encapsulating the entire data model in a single entry in the model. Wrapping the data model in an effects manager would allow changes to the data model to be handled via commands from the UX just as commands to a server would be, but this has its own challenges since effects managers can’t use effects from other effects managers.

Mark

2 Likes

I probably could have chosen a better piece of code to try and demonstrate where I thought the TEA pattern was needed. It was still interesting to see that it really wasn’t in this case.

elmq solved that problem by having the effects manager as a middleware. A Cmd from a non-effects module on one side becomes a Sub in another non-effects module on the other side, and from there other effects can be invoked.

The problem with elmq, other than the fact that it was non-publishable as a black market effects module, was that the type of messages could not be made polymorphic, so everything was sent/received as json Values; you had to write Decoders and the type checking was not so effective.

Having polymorphic message channels like Concurrent ML would be a very interesting feature to consider for Elm.

At the moment I am wondering if the Day convolution can be applied to 2 Elm Programs, and also allow for those Programs to set up typed message channels between them. That is my next coding experiment in Elm :-).

So here is a rough sketch of what I was thinking.

There are 2 programs, one is subscribed to a clock tick, it sends this as a message to another program which receives it and displays it.

I used the Html.Alternative.Program type from joneshf/elm-comonad as a starting point, just because it already had an implementation of the Day convolution. I think I would actually not use this Program type, but the Html.Program type.

My authentication package could be a headless Platform.Program(WithChannel), and my UI an Html.Program(WithChannel). I could evolve the channel structure a bit so that these two programs end up with nice APIs into each other. The UI can send/invoke login, logout, and unauthed and the authentication program can send/invoke a small model describing the current authentication state.

Nice clean APIs, type checking on the messages, each program has zero knowledge of the internal model of the other and easy to integrate with a suitable program convolution function.

The relevant point is that shown doesn’t change based on any ControlBar state—it’s something strictly controlled by the caller. The typical Elm API for hiding something is to decline to render it. There’s no Html.Button.hide, for example—if you don’t want a button there, don’t call Html.button!

I changed the API to be like button: if the caller wants a ControlBar, they call ControlBar.view; otherwise, they don’t.

For the love of all that is good in the world, please only when the design calls for it. :scream:

That’s more or less where I settled but with the caveat that if changing APIs is going to cause excessively widespread ripples — i.e., across multiple projects because the view function is actually being written by a team trying to establish corporate UX standards — or within a project with multiple engineers there are engineers who will try hard to avoid updating either lots of code or other people’s code, then one may want to consider use cases beyond the present case. I’ve worked with engineers who would have no concerns with updating something to support animations even if it wasn’t their code and even if it meant potentially touching a bunch of call sites (though adding viewWithAnimations might be a better if inconsistent choice in such a case) and I’ve worked with engineers who would say that they were just trying to use a control bar and clearly Elm sucks compared to whatever they came from before. Code exists in a social context as well as a technical context. Boilerplate causes friction. API updates cause friction. Which one matters depends on social factors.

Mark

P.S. What I suspect is an example of why API minimalism isn’t always the right choice: Have you actually had cause to use everything in your CSS library?

Sure, but I think the social key here is buy-in.

If some people on a team want to use [Elm/React/Vue/Angular] but others on the team prefer the way [a different technoloy] solves a particular problem, there’s going to be friction!

None of these technologies will ever be all things to all people; I think they’re all best served by playing to their own strengths, and leaving teams to figure out which strengths appeal most to them.

Well no, because I don’t use everything in CSS. :smile:

One of the design goals of elm-css is to provide a typed interface to the CSS API. The minimal API to achieve that goal is quite large!

Model is practically the opposite of global mutable variables!

Any code in a program can read and write to global mutable variables. In stark contrast, permission to both read and write Model are extremely strictly controlled. view and subscriptions get read-only access; to them, Model is an immutable constant. Only update has permission to modify Model, and it can create more granular permissions by delegating to functions that work with subsets of Model. Outside of those 3 pure functions, the rest of the program can’t even read it!

It’d be more accurate to say that Model has nothing in common with global mutable variables.

The implication here is that “structuring Elm programs as nested M/V/U triplets is a reasonable choice, but how to do it nicely is an unsolved design problem,” even though the language’s foundational design, Occam’s Razor, and a mountain of historical evidence all say the reality is actually that “structuring Elm programs as nested M/V/U triplets is a mistake.”

The most recent case in point is earlier in this thread. I showed how to refactor an API that used a nested Model/View/Update design into a much simpler API that did not use it. How lucky was I that out of the 1 (one) motivating code example for nested M/V/U, it turned out there was a simpler, nicer API within reach…that was obtained by doing nothing more than declining to use nested M/V/U in the first place?

This was not a lucky coincidence; I’ve seen this so many times, I’ve lost count! Nested M/V/U turning out to be a mistake in practice is normal.

As an aside, I never could have imagined the teaching challenge that our industry’s OO monoculture would create for Elm. It’s nobody’s fault—people come from OO backgrounds and reflexively follow OO design patterns. I know that feeling well, since I did it myself when I was starting out!—but it’s surprising nonetheless.

I do recognize this as being true BTW, I just did not make a good job of stating it clearly enough in the opening post. However, as I can see it used in the elm-spa-example, I think it is important to acknowledge that it is a valid technique and there are occasions where we resort to it. (And as you can see from my code, I resorted to it too soon).

I’ll start a new thread I think to discuss the Program convolution - now that I’ve gone from grasping at ideas to a rough outline I think I have something that will work nicely for what I am trying to do with my auth package and its API. I will also try and provide some justification as to why I think it is a reasonable choice for what I am trying to do with that.

1 Like

BTW, would you care to comment on what motivated you to use it in the elm-spa-example, in the few places that you did use it?

It is interesting to learn about what the limits of the techniques to avoid it are, and what factors justify its use. Or perhaps you would consider refactoring elm-spa-example to completely avoid it, were you to do it again?

I count 8 places it is used, 7 of the pages under Pages/ are child MVUs, and so is Views/Article/Feed.elm. I must be counting differently to you as you say there are only 3 places, but perhaps you are counting the parent MVUs.

I feel I should also comment on this too. OO design patterns are to a significant degree influenced by Parnas’ modular design principles. Those principles manifest themselves in OO as encapsulating state within the object, and providing a minimal set of methods on the object to interact with that state. That is related state and functionality is kept together (high cohesion) and the API is kept minimal (low coupling).

Even though Elm takes a different path to OO, I still think that Parnas’ modular design principles can find valid and useful expression in Elm. Elm provides 3 levels on which this can be explored; at the functional language level, at the module level and at the package level. The package level is the one that demands it the most, since each package really is a module of re-usable software.

It would be quite interesting to explore the idea of a retraining program from OO to Elm. First understand the principles of OO in the context of Parnas, then explore all the ways that Parnas can be expressed in Elm, and out of those, which ones lead to the most productive coding patterns that allow safe and better software to be written.

To me, Parnas is the master mental model that runs through everything I do that is related to computer software.

1 Like

Sure!

At least in Elm 0.18, SPA routing between pages is by far the most common use case. (I expect that in a future release, code splitting will result in an alternative API for doing this, but Html.map seems to be the right approach for today.)

The other two modules (besides Main) that use Html.map are Home and Profile. Both of them use it for Feed.

The reason I used it for Feed is that Feed has 6 Msg values and their update logic is nontrivial.

I default to not creating Feed.Msg and Feed.update, because they’re usually not worth the cost, but this is one of the rare cases where the alternative would be worse! In the alternative world, Feed.view would have to accept 6 different toMsg functions, every page that used a Feed would be required to add 6 constructors to its own Msg, and the corresponding update clauses would have to call ~6 different Feed functions because their implementations are nontrivial.

Because of the high Msg count and nontrivial update logic that goes with them, in this case—unlike every other case in the app—the comparatively lightweight approach was to use Feed.Msg and Feed.update…even though everywhere else in the project, a view function alone was the better API.