user5960886
user5960886

Reputation:

How choose which classes to make an instance of when overloading operators in haskell?

I am currently trying to overload the basic math operators (+,-,*,/, negate) and ordering operators (<,>, >=, <=, ==) for the made up data type Pair. Currently, I have an instance of num which requires me to overload its visible members, but I am having hard time figuring out if I need to make multiple instances of different classes like Fractional, Ord, and so forth, or if there is a better method to overload these specific operators.

Code Example:

 data Pair = Pair (Int, Int) deriving (Eq, Show)
 instance Num Pair where
    Pair (a,b) + Pair (c,d) = Pair (a+c,b+d)
    Pair (a,b) * Pair (c,d) = Pair (a*c,b*d)
    Pair (a,b) - Pair (c,d) = Pair (a-c,b-d)
    Pair (a,b) / Pair (c,d) = Pair  (a/c , b/d) // throws an error for this line.
    abs    (Pair (a,b)) = Pair (abs a,    abs b) 
    signum (Pair (a,b)) = Pair (signum a, signum b) 
    fromInteger i = Pair (fromInteger i, fromInteger i)

Upvotes: 1

Views: 141

Answers (2)

leftaroundabout
leftaroundabout

Reputation: 120711

Some preliminary ranting

Generally in Haskell, you should avoid thinking of “overloading an operator”. Type classes are more than syntax for overloading operators; rather think of them as abstract axiomatisations of data types. Now if that sounds scary forget the word axiom and consider an example.

A number type N is a type whose values a, b, c... fulfill properties (laws) such as

a + 0 = a
a * 1 = a
a + (b + c) = (a + b) + c
abs a * signum a = a

It is properties like these which allow you to truely write num-polymorphic code (i.e. that'll work with any numerical type) and still be certain that you'll get meaningful results in the end for all of them. That really is the power of overloading. Just being able to re-use a nice short identifier such as + may be handy in itself, but really wouldn't gain you much over custom operators like +. for specialised num types (that's how O'Caml does it).

Now, for such laws to be formulable, you need to have all these operators available, and that's why we have classes which require us to not just overload whichever operator we fancy right now, but a whole coherent group of them.

Start reading here if you haven't already...

Classes aren't some special built-in magic. You can easily define your own classes, or, for standard classes like Num, look up where they're defined. The easiest way to find class definitions (or any other library declarations) is the Hayoo seatch engine. It will for e.g. + guide you right away to the (+) :: a -> a -> a Class Method on hackage. There you see how + is contained in

class  Num a  where
    {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
    (+), (-), (*)       :: a -> a -> a
    negate              :: a -> a
    abs                 :: a -> a
    signum              :: a -> a
    fromInteger         :: Integer -> a

So if you want to overload the + operator you'll need to define all of (+), (-), (*), negate, abs, signum and fromInteger.

Whoa, but maybe you didn't even want something like abs!

Very reasonable. But then your type is evidently not really a number type! IMO you shouldn't make anything a Num instance that's not just a representation of a single number. Note that there are simpler classes available: you probably want AdditiveGroup

class AdditiveGroup v where
  zeroV :: v
  (^+^) :: v -> v -> v
  negateV :: v -> v
  (^-^) :: v -> v -> v

This is a class for more general vector spaces. You can easily make your type an instance:

instance AdditiveGroup Pair where
  zeroV = Pair (0,0)
  Pair (a,b) ^+^ Pair (c,d) = Pair (a+c, b+d)
  Pair (a,b) ^-^ Pair (c,d) = Pair (a-c, b-d)
  negateV (Pair (a,b)) = Pair (-a ,-b)

You notice that this class doesn't have a multiplication. Well, it turns out that mathematically, multiplication doesn't really make all that much sense on tuples. There are other kind of multiplications though (scalar multiplications) that make sense on vector spaces. Browse the documentation of the vector-space package.

On a different note

Also consider whether you even need AdditiveGroup instance. Daniel Wagner uses in his answer a class that's way more general than numbers, and can be used for all kinds of container-types. Argumably, your Pair is more of a container for numbers, than a number per se. Therefore, I recommend only implementing

data Pair a = Pair a a
instance Functor Pair where
  fmap f (Pair x y) = Pair (f x) (f y)
instance Applicative Pair where
    pure v = Pair v v
    Pair f g <*> Pair x y = Pair (f x) (g y)

Functor is really simple: it allows you to apply a function to all elements in a container. Applicative basically allows you to cross-apply functions between two containers.

With these instances you can, instead of using numerical operators directly, add pairs by writing liftA2 (+) (Pair 1 2) (Pair 3 4) whenever you need that operation. A bit more verbose, but also a good deal more explicit!


Frankly, it's better not to do that too often though. Classes as I said are meant to represent deep mathematical axiomatisations; this is not possible for anything that OO programmers would write a class for. Nor is it necessary to write custom classes all the time: data alone can get you pretty far in a functional language.

Actually it's sufficient to implement either (-) or negate. That's what the MINIMAL pragma tells you.

Upvotes: 4

Daniel Wagner
Daniel Wagner

Reputation: 152867

I think I'd probably write something like this:

{-# LANGUAGE DeriveFunctor #-}
import Control.Applicative

data Pair a = Pair a a deriving (Eq, Ord, Read, Show, Functor)
instance Applicative Pair where
    pure v = Pair v v
    Pair f g <*> Pair x y = Pair (f x) (g y)

instance Num a => Num (Pair a) where
    (+) = liftA2 (+)
    (-) = liftA2 (-)
    (*) = liftA2 (*)
    negate = liftA negate
    abs    = liftA abs
    signum = liftA signum
    fromInteger = pure . fromInteger

instance Fractional a => Fractional (Pair a) where
    (/) = liftA2 (/)
    recip = liftA recip
    fromRational = pure . fromRational

You can learn which class a particular operation is related to by looking at its type:

(/) :: Fractional a => a -> a -> a

Hence if you want to support (/), you should look into implementing the Fractional class.

Upvotes: 3

Related Questions