On designing with sum and product types
I understand product types in statically-typed FP languages; they're records:
I understand sum (or union) types; they're cool:
As I learn Elm, I'm been having considerable trouble combining the two: should I model the problem as a sum type inside a record? as records in a sum type? For a good while in my Eecrit project, I bounced around alternatives like these:
The worst part was that I was developing iteratively, and new features kept breaking my model: I didn't find my design converging in the way that I'm used to from TDD and dynamic languages (be they OO like Ruby or FP like Clojure).
At one point I'd settled on a record something like this for a
That worked well for editing existing animals (which was the first type of editing I implemented). But then I implemented the "you can create one new animal" story. The same record almost worked, except there might not be an
(Editorial comment: the "this is like that except for..." problem is the core software design problem in messy domains, which are ones where all the "except for"s keep coming and can't be handled by insisting the business be "rational" - i.e., think the way we do.)
I wanted to make the distinction between the two kinds of form concrete - directly represented in the types - but I didn't have the energy. (I was already well behind my self-imposed schedule.)
So I just added a
... and I smeared
case form.originalAnimal of expressions throughout the code. That was particularly grating for places where I knew one of the cases was impossible. But I got the app working.
At some later point, I was whining on Twitter that the FP literature is really weak on dealing with messy systems. Lance Walton asked for an example. I provided this one. Mark Seemann suggested that I push all the special-case code into one place. It still might not be the Right Model, but at least that "isolates the place where you have to implement changes".
This is absolutely standard OO design practice, right? Put the functions next to the data; hide the data behind the functions. It's also what I'd naturally do in Clojure or Elixir.
However, my first pass at trying it made me think it wouldn't work:
Types.Form would end up knowing too much about the rest of the program. Also, I was hung up on the idea that if I changed my app, I should do so in a way that made illegal combinations of values impossible.
Nevertheless, after one restart, it worked out pretty nicely. That is, the functions make sense: with one exception, they're "about" the Form, not about how other code uses it. Even that one function is not horrible. It's used like this:
(Note: line 5 is idiosyncratic. I use pipelining heavily so as not to have to keep messing with
(model, cmd) tuples.)
givenOriginalAnimal isn't great, but I don't have a better one. The expression is meant to convey that if there's an animal attached to the form, that animal replaces the form in the display. Left only implied is that there will always be an associated animal. (But if the impossible happens, the whole thing is a no-op.)
Here's the code for
You can find the whole file here.
To me, the only interesting thing about this episode (other than why I didn't do this in the first place) is how like this solution is to OO practice. This seems exactly what I'd do in Ruby. Perhaps after a time I might decide that distinguishing two subclasses might make sense. That'd be a pretty straightforward refactoring.
What I want to discover in Elm (and, someday soon, Purescript) is how to do that gradual, undisruptive improvement. I think that will require surfacing things that are underdiscussed and perhaps unpopular in the statically-typed FP community:
- Patterns as "attractors" in design space.
- Satisficing as a design goal, or: valuing types that are not "lawlike" in the way that monads are but are nevertheless useful.
- Relatedly, acceptance that designs get (haltingly!) better over time, and that such a process of design is worthy of deep study.
I very much want to see those things happen, and am tempted to charge forward on them myself. But the social/community issues are crucial, and I doubt I'm deft enough to handle them.
P.S. I've also tried using structural typing toward this same end:
It didn't work well.