generic-lens
generic-lens copied to clipboard
First-class record construction.
One of the things that often annoy me in Haskell is that record construction is not a first-class element. For example, imagine this simple record construction:
john = Person { name = "John", age = 42 }
There are two problems with this definition:
-
{ name = "John", age = 42 }
cannot be assigned to a variable nor reused anywhere else. -
It cannot be composed with other record constructors to get the union of all the fields. If you use lenses, you can compose record updates (through setters) but not record construction AFAIK.
Given the record/hlist isomorphism it should not be too hard to implement a first-class record "builder". Internally it can be expressed as an hlist (with convenient user-facing types and operators to set and compose fields) and record construction would consist of reordering the elements of the list and converting to the record representation.
This feature would allow for a form of composition that goes beyond the subtype/supertype relation. I have often needed it, but I have always thought this was impossible to achieve in Haskell. Now I realized it might be doable, and generic-lens
already has the necessary infrastructure. What do you think of the idea?
This should be possible, and sounds useful. The reason I wrote the HList machinery in the first place is to make expressing things like this easier! The main complication is the ordering of the fields, but that could be solved as well, with a well thought-out interface. Do you have any suggestions of what it might look like?
I think users should be free to provide the fields in any order, otherwise this feature would not be useful enough. But this would make type inference hard.
One idea could be to provide a function that takes the symbol as first type-parameter, to allow for easy type-application.
newtype Builder (as :: [(m, Type)]) = Builder (List as)
builder :: Builder '[]
builder = Builder Nil
-- s comes first, so it can be applied with TypeApplications
field :: forall s a as. a -> Builder as -> Builder ('(s, a) ': as)
field x (Builder xs) = Builder (x :> xs)
This should make the syntax a little bit nicer:
builder & field @"foo" 123 & field @"bar" True
In my idea the fields should only be reordered at record construction. Until that moment users should be free to add fields in any order. Do you think it is possible?
Yes, I like that. I think this should also allow us to "open" records and chain multiple polymorphic modifications before ultimately "closing" them:
data Foo a = Foo { wurble :: a, burble :: a } deriving Generic
convertFoo :: Foo String -> Foo Int
convertFoo foo = foo & open & field @"wurble" %~ length & field @"burble" .~ 10 & close
(Here I use the field
that already exists in the library)
But yes, like you said, inference is going to prove somewhat challenging here. Your suggestion of having a fixed order and providing a smart reordering function is certainly possible, and seems quite elegant.
Maybe my field
function should be renamed to something less ambiguous :)
The open/close idea is cool. I thought this kind of polymorphic update was already possible, but a quick test in GHCI showed me that it is not the case.
What I like about this HList isomorphism is that it can give you the flexibility of extensible records, without the inconvenience of needing a whole new "language" and ecosystem. I'm pretty sure there is still a lot to be explored in this sense.