My question is, if you do this thoroughly and end up with plain old functions getting plain old record parameters containing the methods, is GHC capable of seeing through enough calls to figure out that all these function arguments are actually fully determined by typeclass instances, and thus by types, and thus they can be eliminated based on the types alone?

Maybe it is, this is a genuine question, although I can see how my original reply reads like an implicit "Surely," 😀

On Thu, Dec 11, 2025, 21:14 Tom Ellis <tom-lists-haskell-cafe-2023@jaguarpaw.co.uk> wrote:
On Thu, Dec 11, 2025 at 09:07:28PM +0800, Gergő Érdi wrote:
> Doesn't this undermine a lot of the specialization-based optimizations to
> get rid of runtime dictionary passing?

If it does then my idea is dead on arrival for anything performance
sensitive. But why would it?  I asked the question because I was
concerned that classes with a single method containing a dictionary
might have performance overhead compared to "unpacking the dictionary"
in the class body, i.e. the difference between Simon's

    class C a where { op1, op2 :: a -> a }

and my

    data CD a = MkCD { op1Impl :: a -> a, op2Impl :: a -> a }
    class C a where cImpl :: CD a

Simon says there is no overhead. What specialization-based
optimizations are you thinking of that may not apply to my version?

Tom

> On Thu, Dec 11, 2025, 21:01 Tom Ellis <
> tom-lists-haskell-cafe-2023@jaguarpaw.co.uk> wrote:
>
> > On Thu, Dec 11, 2025 at 09:15:16AM +0000, Simon Peyton Jones wrote:
> > > Classes with exactly one method and no superclass (or one superclass and
> > no
> > > method) are called "unary classes".  And yes, they are still implemented
> > > with no overhead.
> > >
> > > See this long Note:
> > >
> > https://gitlab.haskell.org/ghc/ghc/-/blame/master/compiler/GHC/Core/TyCon.hs#L1453
> >
> > Super, thank you for the reference.
> >
> > > > I find type classes very difficult to evolve in a way that
> > > > satisfies my stability needs. Part of the reason for this is that
> > > > type classes as typically used don't really permit any form of
> > > > data abstraction: you list out all the methods explicitly in the
> > > > class definition.  There is no data hiding.
> > >
> > > That's odd.   Can't you say
> > > ```
> > > module M( C, warble ) where
> > >    class C a where { op1, op2 :: a -> a }
> > >
> > > warble :: C a => a -> a
> > > warble = ...
> > > ```
> > > and now a client of `M` can see `C` and `warble` but has no idea of the
> > > methods.
> >
> > That deals with one direction across the abstraction boundary: the
> > elimination form.  We also need introduction forms as you point out:
> >
> > > Of course if a client wants to make a new data type T into an instance
> > of C
> > > then they need to know the methods, but that's reasonable: to make T an
> > > instance of C we must provide a witness for `op1` and `op2`.  So your
> > > teaser is indeed teasing.
> >
> > Right, and once witnesses have been provided for `op1` and `op2`, the
> > client is now coupled to that interface.  Here's what I'm suggesting
> > instead:
> >
> >     -- | Crucially, CD is abstract
> >     module M( C, CD, op1, op2, warble, Ops(..), cdOfOps ) where
> >
> >     data CD a = MkCD { op1Impl :: a -> a, op2Impl :: a -> a }
> >
> >     class C a where cImpl :: CD a
> >
> >     warble :: C a => a -> a
> >     warble = ...
> >
> >     op1 :: C a => a -> a
> >     op1 = op1Impl cImpl
> >
> >     op2 :: C a => a -> a
> >     op2 = op2Impl cImpl
> >
> >     data Ops a = MkOps { opsOp1 :: a -> a, opsOp2 :: a -> a }
> >
> >     cdOfOps :: Ops a  -> CD a
> >     cdOfOps ops = MkCD { op1Impl = opsOp1 ops, op2Impl = opsOp2 ops }
> >
> > And clients can now define
> >
> >     instance C T where
> >       cImpl = cdOfOps MkOps { opsOp1 = ..., opsOp2 = ... }
> >
> > But I can also provide more helper functions such as these:
> >
> >   cdOfId :: CD a
> >   cdOfId = MkCD {op1Impl = id, op2Impl = id}
> >
> >   cdOfTwice :: (a -> a) -> CD a
> >   cdOfTwice f = MkCD {op1Impl = f, op2Impl = f . f}
> >
> > So instances can be written briefly, in a way that is typically done
> > with DerivingVia:
> >
> >   instance C T2 where
> >     cImpl = cdOfId
> >
> >   instance C Bool where
> >     cImpl = cdOfTwice not
> >
> > Why do this? Suppose I realise that it is a law that `op2` must
> > *always* be `op1 . op1`.  Then `cdOfOps` becomes risky, and I can add
> > a warning to it, deprecate it, and subsequently remove it if I want.
> > Everything else, including `cdOfId` and `cdOfTwice` are safe, and can
> > remain unchanged.
> >
> > There is no easy path if `op2` is a method.  I can't add a warning to
> > it, because it's still safe to *use* it and client code will be using
> > it.  It's just unsafe to *define* it.  Ideally it should be lifted out
> > of the class definition and defined as `op2 = op1 . op1`, but that
> > breaks every client who has a C instance defined, without the ability
> > to provide a smooth deprecation cycle.
> >
> > Anyway, I hope to be able to write this up in more detail in the near
> > future, including the benefits I see we would have had during AMP,
> > Monad Of No Return, and the proposal to remove (>>) from Monad, if
> > this approach had been standard practice.
_______________________________________________
ghc-devs mailing list -- ghc-devs@haskell.org
To unsubscribe send an email to ghc-devs-leave@haskell.org