
Today I thought it was about time to simplify how new 'things' of a certain kind are added to the system. These things are some a cross between an event and an assertion of a fact in a rule based system. There are many different kinds of these things. I already have more than a dozen commonplace ones, and I expect there's a much larger number of more specialized ones that a user will want to add on their own. While they start out quite differently, they end up satisfying a common interface and follow the identical three or four state lifecycle. This sounded like a type class to me, and in fact, easily implemented as such.
I hardly ever use typeclasses, I've never used existential types or GADTs, and it's worked fine for me for many years. Maybe just a difference in programming style, or the sorts of things I write, but implies at least that you can get very far not using any of that stuff. If each of your things have the same 3 or 4 states, can you make a state into a value, and compose them? E.g. 'thing1 = state1 <> state2 <> thing1state where thing1state = ...' and state1 and state2 are defined in a library. If you have lots of different ways to take A to B and want to let the caller configure it, then just pass an A->B function. If you want to configure an unpredictable subset of things, then maybe make a default record and pass 'default { aToB = customVersion }'. If each function depends on a configuration environment that you want to inherit from callers, then maybe put that record into a Reader. In my case, the main design decision winds up being the balance of data (i.e. records with values or functions) and code (i.e. functions that do parts of what you want and can be composed together in various ways). Code is extensible and flexible but can't be manipulated, data is inflexible (in that you have to hardcode some kind of "schema"), but that means you can write functions to transform it.