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