在用户定义类型和现有类型之间定义已经存在的(例如在 Prelude 中)运算符的正确方法是什么?

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

假设我有一个包装现有类型的自定义类型,

newtype T = T Int deriving Show

并且假设我希望能够将 T 相加,并且将它们相加应该导致将包装值相加;我会通过

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

我认为到目前为止我们都很好。请告诉我到目前为止是否有主要问题。

现在假设我希望能够将 T 乘以 Int 并且结果应该是 T 其包装值是前者乘以诠释;我会选择这样的东西:

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

这显然是行不通的,因为 class Num 声明了 (*) :: a -> a -> a,因此要求两个操作数(和结果)都是同一类型。

即使将 (*) 定义为自由函数也会造成类似的问题(即 (*) 已经存在于 Prelude 中)。

我该如何处理?

至于为什么会出现这个问题,我可以设如下

已经经常询问非常相似的例子,答案是这 不是 数字类型,因此不应该有 Num 实例。它实际上是一个 vector space 类型,因此你应该定义

{-# 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

那么你的T -> Int -> T运算符就是^*,就是flip (*^).

这也导致了在重载具有不同含义的标准运算符时应该做的更一般的事情:只需将其设为 单独的 定义即可。你甚至不需要给它一个不同的名字,这也可以使用 qualified 模块导入来消除歧义。

只是请不要不完整地实例化 类,尤其是 Num。当有人对这些类型使用泛型函数时,这只会导致 php-ish 混淆,它编译得很好,但当调用代码期望 Num 语义但类型实际上无法提供时,它会在运行时可怕地中断那。