Skip to content

GeneralizedNewtypeDeriving should not require that constructor be in scope

As I learned perusing !1916 (closed) (but you don't need to read that MR), GeneralizedNewtypeDeriving requires that the constructor of the newtype in question be in scope. Yet this isn't really necessary:

A.hs:

module A where

newtype N1 = MkN1 N2

newtype N2 = MkN2 N1

instance Eq N2 where
  (==) = const (const False)

B.hs:

{-# LANGUAGE DerivingStrategies, StandaloneDeriving, GeneralizedNewtypeDeriving,
             DerivingVia #-}

module B where

import A ( N1, N2(..) )

import Data.Coerce

-- This fails:
-- deriving newtype instance Eq N1

-- This succeeds:
-- deriving via N2 instance Eq N1

-- This succeeds:
instance Eq N1 where
  (==) = coerce ((==) :: N2 -> N2 -> Bool)

I would think that the three possible instance declarations in B.hs would all be completely equivalent. Yet the first is rejected while the last two are accepted.

This example is indeed silly, but it's possible to make mutually-recursive newtypes that are not silly (by using e.g. Maybe or Either to break the loop).

Conclusion: The newtype strategy should not require the newtype constructor to be in scope. Instead, it should rely on the Coercible machinery to trigger the usual case where we need the constructor in scope. Of course, we should make sure that the error message is sensible when this happens.

To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information