Say you’re writing a library and you need to expose records in the interface…
data Foo = Foo
{ _fooX :: A
, _fooY :: B
}
Say you know you will end up with records within records, so you know lenses/optics are a thing that will come in handy for the user.
Foo
as written is written in the style that was definitely a recommend practice when I first started using Haskell, back when GHC 8 was new. It avoids duplicate record fields, and fields start with underscores so makeLens
can generate lenses that take the un-underscored names.
However, these days there are all manners of language extensions around records, but many seem opinionated. They’re fine when your working on your own thing but their use often enforces an opinion on users.
I have no idea what combination of language extensions is actually good practice for user-facing records in modern Haskell.
DuplicateRecordFields
Should I just use DuplicateRecordFields
and write:
data Foo = Foo
{ _x :: A
, _y :: B
}
…while knowing full well there will be other records with an _x
field?
Obviously this still leaves record selectors as ambiguous, along with record updates, sometimes, but it will also lead to name clashes when generating lenses with makeLens
.
I could use lens
's TH tools for generating classy lenses, but that doesn’t work for non-simple lenses and polymorphic records. Non-simple lenses for polymorphic fields can’t share names.
I could use optics
instead and use their OverloadedLabel tech, I think this works with polymorphic records, but then I’m forcing the user into the opinionated choice of optics
over lens
.
NoFieldSelectors
Field selectors kind of suck! If I ditch field selectors then I can define Foo
as:
data Foo = Foo
{ fooX :: A
, fooY :: B
}
And still use fooX
and fooY
as an identifier for lenses. That is neat.
Obviously you can also use this in conjunction with DuplicateRecordFields
, and in fact this prevents issues with ambiguous duplicate selectors…
But then I’m forcing on the user the opinionated choice of not having field selectors. If they want them (for some confounding reason) then it is annoying work to write them by hand.
If I don’t turn on NoFieldSelectors
it is not possible to export or import the fields without the selectors.
If I turn on NoFieldSelectors
but then write selectors anyway, the user can choose whether to import selectors… but then I have to manually write selectors!
At least with NoFieldSelectors on, field selectors are easy to define (or replace) with OverloadedRecordDot
's (.field)
syntax.
OverloadedRecordDot
Overloaded record dot syntax is great in that I can use it without forcing it on others, and I don’t have to do anything to make sure they can use it…
But suppose I want to use NoFieldSelectors
after all, but I don’t want to export manually made selector functions. Only doing this would make my records harder to use records.
You need either lenses/optics or OverloadedRecordDot
to get back the ability to refer to record fields without record pattern matching…
But suppose the user of my library is a beginner, fresh out of Learn You A Haskell, expecting there to be field selector functions. They won’t understand lenses and they won’t know the OverloadedRecordDot
extension. I need to document how to actually use my records with examples if I want my library to be accessible to these sorts of people.
Do I tell the user to use OverloadedRecordDot
with examples? Or do I tell them to use an optics library, with examples. Which optics library do I recommend? Opinions!
Optics
Suppose some of my records need to have their constructors and fields hidden to maintain some invariant.
Now the user can’t make optics for my types if I don’t do it, and I can’t split off the optics definitions to a separate package because they need access to the record constructors.
lens
adds a lot of dependencies, but actually I can hand-write most optics without any lens
dependencies.
Alternatively I could use microlens-th
to generate lenses with a lot less dependencies than lens
.
Or I could add an optics
dependency and use that library to generate optics
optics.
Actually, you can turn lens
style VL optics into optics
optics… so you could just generate VL optics and let the user convert them to optics
optics if they’re needed…
But does that come with a performance cost? Does anyone know?
If I want to provide optics for my user, what kind of optics should I provide? Opinions!
Record sums
NoFieldSelectors
means no partial selector functions when you define sums of records like:
data Foo
= Foo { x :: A, y :: B }
| Bar { x :: A }
That’s cool… but record updates and OverloadedRecordDot syntax can still be partial.
So, should we still not mix sum types and record syntax even with NoFieldSelectors
on?
Best Practices?
What are the best practices for modern Haskell for public libraries that are friendly to both the beginner and the seasoned Haskeller, that works for the broadest majority of use-cases?
Should I provide selectors or turn off field selectors and embrace OverloadedRecordDot with both hands?
Or instead of OverloadedRecordDot should I take a lens-first approach?
Should I prefix record fields with the type/constructor name so I don’t have trouble with lenses and polymorphic records, or should I use DuplicateRecordFields and hand-write prefixed lenses for the polymorphic fields?
Or should I use optics
and OverloadedLabels?
Or is the best practices circa 2024 still just:
data Foo = Foo
{ _fooX :: A
, _fooY :: B
}