One thing that nags me about extensible records in their current implementation is that a record field name is not tied to a module, the way that a (potentially opaque) type constructor is, and thus lacks semantic import:
module Widget exposing (Widget, Doodad)
type Widget
= GoodWidget ...
| BadWidget ...
type alias Doodad =
{ length: Float
, ...
}
In this example, everything about “Widget”, “GoodWidget”, and “BadWidget” is implementation controlled and possibly made/kept opaque by the Widget module. It’s context and semantics can be well defined, and third parties have no footing with which to make incorrect assumptions about these semantics: the compiler can be made to defend them as the API evolves.
Doodad is controlled by Widget in principle, but the “length” field name (and any other field names) are not so much. This nags me because a Doodad’s “length” may mean something very different from a song’s length (different implied units and measurement dimensions), for example.
Thus having a function like:
doSomethingWithLength : {a | length: Float} -> {a | length: Float}
doSomethingWithLength input =
{ input | length : input.length + 2 }
can easily be made to confuse two semantic concepts of length that may have good reason to be distinct, lends itself to being invasive of implementation details, and IMHO can lead to many kinds of sloppy habits.
While I agree that flat models have many things to speak to their convenience, I also like the semantic certainty of types that can be made opaque. In particular I feel that if any function which needs to operate upon “part of” a larger value may be best expressed operating upon a type (and thus possibly being a method from that type’s module, or a helper module thereupon) and then said type can be an element of the larger aggregate object (which implies depth instead of flatness).
The justification here being that a function implies a behavior and a certain expectation upon the nature of the inputs, which in turn is best modeled as a semantic fact. “I make things longer” (by way of altering a float labeled “length” in a record somewhere) does a poor job encapsulating this idea, because it washes out all context of unit, dimension, why does the thing need to be longer, how much longer, what if our needs change, what if our understanding of the thing with length evolves over time (perhaps now it also has a width? Or a tempo?) etc.
It feels like extensible records also do a lot to encourage the leaky abstraction of primitive typing. Primitive types are (over)broad and thus more easily lend themselves to reuse and partial access through different functions that try to be everything to everybody.
Now I don’t mean to demonize extensible records or ask that they go away, but I just feel it is worth the warning not to allow that pattern to go to one’s head. In particular, if one needs to use a function on a portion of a data object I think it’s always worth at least considering making that portion of the data object into it’s own distinct type with explicit semantics.