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
}