Search code examples
haskellconstraintstypeclassnewtype

In Haskell, how to attach constraints to a parametric newtype, so that they automatically apply to any class instance that uses it?


Assume I have a parametric type defined like this:

newtype FancyComplex a b = FancyComplex (a, b)

I intend to never use this newtype for any other parameters than numeric ones. I mean that for whatever implementation I might do, I know that parameters a and b will always be an instance of Num .

I read in this question that you can do this: Can a typeclass constraint be used in a newtype definition?

{-# LANGUAGE RankNTypes #-}
newtype (Num a, Num b) => FancyComplex a b = FancyComplex (a, b)

However this is not enough. If I write any class like this:

class StupidClass x where add :: x -> x -> x

Then I should be able to write

instance StupidClass (FancyComplex a b) where
    add (FancyComplex (a, b)) (FancyComplex (a', b')) = FancyComplex (a+a', b+b')

But no GHC will tell me saying I did not enforce the Num requirement. So I am forced to do this everytime:

instance (Num a, Num b) => StupidClass (FancyComplex a b) where
    add (FancyComplex (a, b)) (FancyComplex (a', b')) = FancyComplex (a+a', b+b')

All that writing the constraint in the newtype definition does, is force me to write the constraint explicitly every time. Ok this is still useful in case I forget. But of course I would expect not to have to rewrite the constraint everytime.

How can I automatically and implicitly inherit constraints from the newtype definition? Is this possible? if not, is there a reason why not?

Currently my weak workaround is to define a type alias type FancyComplexReqs a b = (Num a, Num b)

Thanks


Solution

  • This can not be implemented, at least without changing the meaning of a newtype:

    newtype (Num a, Num b) => FancyComplex a b = FancyComplex (a, b)
    
    instance StupidClass (FancyComplex a b) where
        add (FancyComplex (a, b)) (FancyComplex (a', b')) = FancyComplex (a+a', b+b')
    

    In the last line, a+a' needs function + which is a method of Num, so we need to have that at out disposal. I can only see these options:

    1. The + function is stored inside the FancyComplex value. That would work, but the Haskell Report requires this newtype to have the same in-memory representation of a pair. There is no space for an additional pointer.

    2. The Num a, Num b constraint is implicitly added to the instance definition since we need it in the implementation. This can work, but wouldn't it be better to be explicit about it? Having implicit constraints makes the instance harder to read, since there is a constraint even if there seems to be none.

    Now, there is a possible alternative: if you want option 1, and you are fine with a different runtime in-memory representation, then use a data instead:

    data FancyComplex a b where
       FancyComplex :: (Num a, Num b) => a -> b -> FancyComplex a b
    

    In this way each value will store its own pointers to the Num instance. It will require some more memory, but perhaps for your application this is not an issue.