Search code examples
haskellfunctional-programmingoperator-overloadingtypeclassprimitive-types

What is the correct way to define an already existing (e.g. in Prelude) operator between a user-defined type and an existing type?


Suppose I have a custom type wrapping an existing type,

newtype T = T Int deriving Show

and suppose I want to be able to add up Ts, and that adding them up should result in adding the wrapped values up; I would do this via

instance Num T where
  (T t1) + (T t2) = T (t1 + t2)
  -- all other Num's methods = undefined

I think we are good so far. Please, tell me if there are major concerns up to this point.

Now let's suppose that I want to be able to multiply a T by an Int and that the result should be a T whose wrapping value is the former multiplied by the int; I would go for something like this:

instance Num T where
  (T t1) + (T t2) = T (t1 + t2)
  (T t) * k = T (t * k)
  -- all other Num's methods = undefined

which obviously doesn't work because class Num declares (*) :: a -> a -> a, thus requiring the two operands (and the result) to be all of the same type.

Even defining (*) as a free function poses a similar problem (i.e. (*) exists already in Prelude).

How could I deal with this?

As for the why of this question, I can device the following

  • in my program I want to use (Int,Int) for 2D vectors in a cartesian plane,
  • but I also use (Int,Int) for another unrelated thing,
  • therefore I have to disambiguate between the two, by using a newtype for at least one of them or, if use (Int,Int) for several other reasons, then why not making all of them newtypes wrapping (Int,Int)?
  • since newtype Vec2D = Vec2D (Int,Int) represents a vector in the plain, it makes sense to be able to do Vec2D (2,3) * 4 == Vec2D (8,12).

Solution

  • Very similar examples have been asked often already, and the answer is that this is not a number type and therefore should not have a Num instance. What it actually is is a vector space type, accordingly you should define instead

    {-# LANGUAGE TypeFamilies #-}
    
    import Data.AdditiveGroup
    import Data.VectorSpace
    
    newtype T = T Int deriving Show
    
    instance AdditiveGroup T where
      T t1 ^+^ T t2 = T $ t1 + t2
      zeroV = T 0
      negateV (T t) = T $ -t
    
    instance VectorSpace T where
      type Scalar T = Int
      k *^ T t = T $ k * t
    

    Then your T -> Int -> T operator is ^*, which is simply flip (*^).

    That leads also to the more general what you should do when overloading a standard operator with a different meaning: just make it a separate definition. You don't even need to give it a different name, this can also be disambiguated using qualified module imports.

    Just please don't instantiate classes incompletely, in particular not Num. This just leads to php-ish confusion when somebody uses a generic function with those types, it compiles just fine but then horribly breaks at runtime when the calling code expects Num semantics but the type fails to actually offer that.