Haskell New Year Resolutions for 2026

39 Likes

I didn’t know some of these, thanks!

1 Like

I’m already putting the new technique to work :eyes:

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) |]
4 Likes

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:

  1. OverloadedRecordDot needs the constructor in scope, and if you forget you get big errors about missing HasField constructors;
  2. 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 about HasField only gets you good at using OverloadedRecordDot;
  3. 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.

5 Likes

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

2 Likes

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.

1 Like

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
3 Likes

@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.

@Ambrose

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?

3 Likes

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

1 Like

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?

2 Likes

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.

4 Likes