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

1

3

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).
Inwrap answered 9/1, 2021 at 10:0 Comment(0)
K
7

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.

Kernan answered 9/1, 2021 at 10:7 Comment(7)
I'm fairly happy with this answer, but I have a follow-up question: I see that accepting that T being not a Number, and adopting your suggested solution, implies that (T 3) * 3 cannot (probably should not, based on what you write) be made meaningful because at least I have to use another symbol instead of *. Is this correct?Inwrap
@Inwrap If you really want to you can use the symbol *, but whether you do or don't use the same symbol it will be entirely unrelated to the existing one. But it's quite a pain to use two unrelated operators with the same name (you'd have to import the Prelude hiding * and then write things like x Prelude.* y to get to multiplication from Num, or do that with your operator). And normal number multiplication is pretty common, so that's not very convenient. So most people decide to simply give operators like this a different name that emphasizes the connection to the original, like ^*.Heth
@Ben, then in my previous comment I should have written ((T 3) * 3, 3*3) instead of just (T 3) * 3 to make my intention clearer. Based on your comment having that expression being meaningful is not possible. Probably this is just a reflection of the fact that I should not even attempt it, which seems to be in line with the answer we are commenting.Inwrap
@Inwrap Well, there is one option. Which is to completely avoid the Prelude's *, and make your own new version that is general enough able to encompass both T 3 * 3 and 3 * 3. Since the types aren't the same for both of those, it'll require you to make a new class you use instead of Num (at least for multiplication), which means you'll have to write instances for the builtin types as well as your new type. I can write an answer if that's something you're interested in, and don't know how to do?Heth
@Inwrap Worth noting, however, that it in no way makes your new type compatible with the existing operator (anyone else who imports your type won't be able to use * with it; they'll have to also hide the Prelude's * and import your new operator). I've played around with this sort of thing before, and ended up concluding that since Prelude's * is fundamentally a different operation from the one I wanted, it's actually nicer to just use a different symbol for it.Heth
@Heth yeah, I'm getting to the same conclusion. No need for another answer, I guess, as it would express a similar idea as this one.Inwrap
@Heth I've toyed around with “one to ring to rule them all” (pun semi-intended) multiplication operators as well. You can actually get quite close with a “tensor quotient” type family. The · operator subsumes numerical, vector-scaling, and inner-product multiplication, however it's still awkward to use in practice because one of the arguments is a type family that can only be inferred from the other argument and the computation result.Kernan

© 2022 - 2024 — McMap. All rights reserved.