5
votes

I'm defining my own complex number data type as a learning exercise, and I've run into trouble overloading abs along with the other members of Num. As far as I know, only one instance definition is allowed per typeclass, but if I could I'd do something like this:

instance Num a => Num (Complex a) where
    (+) (Complex ra ia) (Complex rb ib) = Complex (ra + rb) (ia + ib)
    (-) (Complex ra ia) (Complex rb ib) = Complex (ra - rb) (ia - ib)
    (*) (Complex ra ia) (Complex rb ib) = Complex (ra*rb - ia*ib) (ra*ib + rb*ia)
    fromInteger r = Complex (fromInteger r) 0

instance Floating a => Num (Complex a) where
    abs (Complex r i) = Complex (sqrt $ r^2 + i^2) 0

or

instance Floating a => Floating (Complex a) where
    abs (Complex r i) = Complex (sqrt $ r^2 + i^2) 0

Because none of the members other than abs require Floating types, and I don't want to limit them to only Floating types, but the abs function is super important, and I don't want to unnecessarily exclude it.
Is there some way I can have the functions (+), (-), and (*) work on all numeric types, while still implementing abs?

According to 7.6.3.4. Overlapping instances in the GHC system guide, multiple instances can overlap if they differ on the type constraint(?) outside of the context (eg instance C [a] and instance C [Int]), with the compiler choosing the most specific instance for a given case, but it doesn't mention anything about only the context differing (eg instance C [a] and instance Integral a => C [a]).

1
Sadly, the typeclass hierarchy for "numbers" (or "basic algebra stuff", or whatever one should call it) was quite badly thought out from the beginning. Maybe you are better off sidestepping it completely for your Complex a? There exists an alternative hierarchy, but it's maybe a bit overkill for your situation. Said differently: You seem to want a principled way to make a typeclass for complex numbers. The standard typeclasses like Num that you are wanting to work with are very ad-hoc :-( - gspr
@gspr: the number hierarchy may not be optimal, but in practice it works quite fine. As far as this use case is concerned – in almost all real-world applications of complex numbers, you would require Floating anyway; complex numbers are mostly useful in a context of roots and/or exponentials (to describe oscillations). - leftaroundabout
@leftaroundabout: Agreed, although one might imagine a practical and useful instance for Ratio Integer too. - gspr
@gspr while such numbers can make sense, they aren't adequately represented by Data.Complex.Complex Rational. hackage.haskell.org/package/cyclotomic has a proper implementation. - leftaroundabout

1 Answers

5
votes

The main source of pain here is that the Prelude's number hierarchy was defined to not be too complex - for most things, it works just fine. This is one of those edge cases where is doesn't really (although as @leftaroundabout points out, I'm not sure there are many applications for a Complex over something that is not Floating).

Your options are to

  • Add a Floating a constraint on Num (Complex a). This is what feels most natural to me and makes the most sense from a type class perspective - shoehorning in instance Num a => Num (Complex a) breaks the Num abstraction because it has no notion of abs.
  • Use a finer-grain numeric hierarchy. The numeric-prelude comes to mind. In that, you will find the following (spread out over multiple modules):

    class (Field.C a) => Algebraic.C a where
      sqrt :: a -> a
    
    class (Ring.C a) => Field.C a where
      (/)           :: a -> a -> a
      recip         :: a -> a
      fromRational' :: Rational -> a
      (^-)          :: a -> Integer -> a
    
    class (Ring.C a) => Absolute.C a where
      abs    :: a -> a
      signum :: a -> a
    
    class (Additive.C a) => Ring.C a where
      (*)         :: a -> a -> a
      one         :: a
      fromInteger :: Integer -> a 
      (^)         :: a -> Integer -> a
    
    class Additive.C a where
      zero     :: a
      (+), (-) :: a -> a -> a
      negate   :: a -> a
    

    In your case, you would be making instances instance Additive.C a => Additive.C (Complex a), instance Ring.C a => Ring.C (Complex a), and instance Algebraic.C a => Absolute.C (Complex a).

  • If I haven't yet managed to convince you to abandon this madness, feel free to check out this page on advanced overlap. Aside from being complex and boilerplate heavy (and needing to turn on a ton of language extensions), this solution isn't quite general (you'll still have to hand-pick which types go to which instance).