- When defining a typeclass that can be generically derived, add the corresponding Generically instance.
- When writing FFI bindings, use the
capicalling convention, where appropiate. - Down with
*for kinds, useTypeinstead. (SeeStarIsType/NoStarIsType). - Consider using the
GHC2024language edition when compatibility with older GHCs (or with other Haskell compilers like MicroHs, as @jackdk mentions) is not an issue. - Add
StandaloneKindSignatures(part ofGHC2024) to clarify complex type constructors. - Use
Listin signatures, instead of the[]type constructor. (SeeListTuplePuns/NoListTuplePuns). - Use
DuplicateRecordFields,NoFieldSelectors, andOverloadedRecordDot. - When writing Template Haskell splices, try to use quotes whenever possible instead of explicit TH constructors.
I didnât know some of these, thanks!
I think it could be simplified further
[| $(varE (mkName name)) |] â varE (mkName name)
[| $(litE (doubleLit val)) |] âlift val
and if you donât care about evaluation order
expr1 <- codegenExpr e1
expr2 <- codegenExpr e2
opFunc <- binOpToName op
[| $(varE opFunc) $(return expr1) $(return expr2) |]
to
opFunc <- binOpToName op
[| $(varE opFunc) $(codegenExpr e1) $(codegenExpr e2) |]
I agree with this in internal code, but disagree with it for Hackage code. MicroHs is gaining steam, and listing explicit extensions beyond Haskell2010 will make it more likely that you donât have to do additional work as a library author to support it.
I tried OverloadedRecordDot and while it looked nice in simple cases, I found it didnât really scale, so I went back to recommending lens + generic-lens (or the optics equivalent) in the work codebase:
OverloadedRecordDotneeds the constructor in scope, and if you forget you get big errors about missingHasFieldconstructors;- It removes the on-ramp for intermediate and advanced lens usage. Since we want people to be able to use powerful optics where justified, It helps to have a bunch of basic and/or nested record field access in the codebase so readers maintain at least basic fluency. If youâre practiced at big errors from
lens, you at least have a skill that you can use for more advanced optics. Getting used to big errors aboutHasFieldonly gets you good at usingOverloadedRecordDot; - It seems to have worse type inference than traditional field accessors, pattern matching, or lenses. This became a problem when we started using the Handle pattern with polymorphic arguments:
-- An example "handle" for a hypothetical "json store". -- Using `myJsonStore.store` with values of two different -- types will cause GHC to throw difficult type errors. newtype JsonStore m where JsonStore :: { -- Note that this function is polymorphic in `a` and -- may need to be called at multiple different `a`s in -- a single function. store :: forall a. ToJSON a => a -> m () } -> JsonStore m
So while -XOverloadedRecordDot is initially a nice-looking feature, it really does seem to âcap outâ at a much lower level than other parts of Haskellâs design, which is a real shame. I prefer to recommend extensions and idioms that scale a bit better.
I am definitely a big fan of using -XDuplicateRecordFields and omitting prefixes from field names. Instead, I provide the record with clean field names and a Generic instance. This lets people use whatever technique they prefer and avoids a direct dependency on any particular optics package.
The first line is
typecheck [GHC-39999]: ⢠Could not deduce âHasField "somefield" MyType a0â
which is fairly understandable, though itâs followed by a lot of irrelevant info about contexts and such before it says NB: There is no field selector 'somefield :: MyType -> a0' in scope for record type 'MyType'. Maybe the error message could be better? Could GHC have a specific message for HasField here with that NB: ⌠line first, and then all the noise that no one reads?
Also, No instance arising [GHC-39999] â Haskell Error Index seems way too general to be helpful.
EDIT: already reported long ago Selecting a nonexistent field using record dot yields error about missing HasField instance ¡ Issue #539 ¡ haskell/error-messages ¡ GitHub
Record dot basically relies on orphan instances. Whereas the optics labels generated by optics-th are canonical with a fundep iirc.
Generally, I agree with you re: record dot. I really donât see much value in the extension compared to the zoo of alternatives.
I guess making Haskell more familiar to the beginner? I donât care about that at all (on this front at least). If youâre onboarding a new Haskeller and Haskell records sans-dot are tripping them up, maybe you need to revamp your interview processes.
Iâm not a fan of lens, so this is a biased response, but here are some of my counterpoints:
- You usually read fields more often than writing them, so Iâd personally take the dot syntax for reading and normal record update syntax when I have to update. Itâs verbose and itâd be nice to have OverloadedRecordUpdate finalized, but itâs not a dealbreaker for me
- If you have polymorphic fields, just provide normal functions. Usually youâll have much fewer polymorphic fields than non-, so use record dot for the fields you can
- You could explicitly write HasField instances to avoid having the constructor in scope
Yes, record dot syntax is less powerful and could be improved, but I still really dont like lens:
- Too many operators, end up with operator soup
- TH to generate lens functions
- Forces users to use lens (with record dot, you could still use NamedFieldPuns, etc.)
- General APIs over prisms and traversables, but this gets confusing
- Super magical if you just copy existing patterns, but difficult to understand how they actually work, and difficult to piece together a new lens expression
@danidiaz Whatâs the benefit of using List? Iâd be open to it if it were available in Prelude, but it seems like I have to import it from Data.List? Also, in most codebases, [Int] is used much more often than '[Int], so maybe itâs just inertia, but itâs nice to just use the special syntax for Lists specifically. Especially since youâre not recommending Tuple2, so why avoid list puns and not tuple puns?
âpunningâ (sharing the same name between type and value constructors) seems to complicate dependent-ish code, as mentioned in the motivation section of this proposal. When working with type-level lists or tuples, it does become awkward in my experience.
I didnât recommend Tuple2 or Unit because they donât exist in current base, they live in ghc-prim for the moment.
We seem to be moving away from punning in other places of base, for example in the constructor for Solo, the 1-tuple.
Another reason for avoiding punning that is mentioned in the proposal, and one that I find convincing, is that punning might confuse beginners.
The latest Haskell Ecosystem Report mentions that a slew of improvements for HasField type errors has been merged, which looks encouraging.
Record dot basically relies on orphan instances.
I donât think theyâre orphans, just a bit special, not unlike other âspecialâ typeclasses like Typeable.
Myself, I do like OverloadedRecordDot, although sometimes Iâm unsure when to use it vs. named puns.
Iâm less sanguine about OverloadedRecordUpdate syntax because, unlike OverloadedRecordDot, it takes preexisting syntax and makes it less powerful in some aspects, like type-changing update. Even if itâs available, I think Iâll choose to handle complex updates with OverloadedRecordDot + optics (although the optics themselves might be built on top of HasField/SetField).
I think they basically quack like orphans at least. You have to import them.
I think many of your complaints around lenses are outdated or incomplete. From a library authorâs perspective, my biggest recommendation is to just expose records with Generic instances and export the constructors. Then uses of lens, optics, and -XOverloadedRecordDot all have an ergonomic way to read fields, and lens/optics users also get a good story for update.
Understandable, but the operators do form a neat little visual language: the @ is an indexed optic, = instead of ~ means it works in a MonadState, etc. This makes them quite predictable and readable, though we never need the really obscure ones. I donât expect to change your mind on this; if youâre deadset against custom operators thatâs totally fine.
No longer true, unless youâre doing things where you canât get a Generic instance (e.g., trying to get prisms into a GADT). Amazonka does this, and it works well, even with some pretty chunky records. Bonus: you no longer lock yourself to an individual optics library, or have to provide foo-lens and foo-optics packages for your users.
No longer true with Generic, you just expose a record and its constructor, and let the user use his preferred method of record access. -XOverloadedRecordDot is placed on equal footing to lenses with this setup, which I think is great.
I wonât deny that there is a learning curve, but SPJâs lens talk (archive version) and Optics By Example are excellent resources to smooth that out.
My primary heuristic, which led me to Haskell and then to favour lenses, is that I want tools that scale smoothly to hard problems. In my experience, -XOverloadedRecordDot is not one of those tools.
I didnât understand your point about explicitly writing HasField instances to avoid constructor imports. Could you please elaborate?
hm can you give an example of how lenses/optics work with Generic?
You could explicitly write
data Foo = Foo { bar :: Int }
instance HasField "bar" Foo Int where
getField Foo{bar} = bar
Redundant? Absolutely. But it does sidestep that particular issue
Given a record like:
data Foo = Foo
{ bar :: Int,
baz :: Char
} deriving Generic
foo :: Foo
foo = undefined
For generic-lens you can import Data.Generics.Product and use type applications:
foo ^. field @"bar"
But the better way, IMHO, is to import Data.Generics.Labels and use the field name as a label in the -XOverloadedLabels fashion:
foo ^. #bar
For optics this stuff is built into optics-core and works pretty much the same way. You can access a field with gfield and import Optics.Label to get the IsLabel instances.
The best part is that library users donât have to beg for someone else to depend on an optics package, run TH, or anything like that. For the common cases, the Generic instance puts everyone on an even footing.
Interesting. I wonder what things would be like if there was a way to automate this. Perhaps a {-# FIELDS #-} pragma on the record or something? This âinstances require constructor importâ behaviour is understandable but sometimes confusing; barbies has a similar issue where Rec(Rec) needs to be in scope for its automatic derivation features to work (because otherwise it canât see Coercible instances). Perhaps thereâs a missing extension or something here, that might lift the availability of automagic instances from data constructors to their type constructor?
Note that only one of those operators (^.) will be needed if youâre planning to use lens exactly as OverloadedRecordDot. All the other operators are for things you canât do with OverloadedRecordDot.
