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

Enr*_*lis 3 haskell functional-programming operator-overloading typeclass primitive-types

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

newtype T = T Int deriving Show
Run Code Online (Sandbox Code Playgroud)

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

instance Num T where
  (T t1) + (T t2) = T (t1 + t2)
  -- all other Num's methods = undefined
Run Code Online (Sandbox Code Playgroud)

我认为我们到目前为止都很好。请告诉我到目前为止是否存在重大问题。

现在让我们假设我希望能够将 a 乘以TanInt并且结果应该是 a,T其包装值是前者乘以 int;我会去做这样的事情:

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

这显然不起作用,因为class Numdeclares (*) :: a -> a -> a,因此要求两个操作数(和结果)都是相同的类型。

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

我怎么能处理这个?

至于这个问题的原因,我可以设置以下

  • 在我的程序中,我想(Int,Int)用于笛卡尔平面中的二维向量,
  • 但我也用(Int,Int)在另一个不相关的事情上,
  • 因此,我必须通过newtype对其中至少一个使用 a 来消除两者之间的歧义,或者,如果(Int,Int)出于其他几个原因使用,那么为什么不将所有这些都newtype打包(Int,Int)
  • 因为newtype Vec2D = Vec2D (Int,Int)在平原中代表一个向量,所以能够做到Vec2D (2,3) * 4 == Vec2D (8,12).

lef*_*out 7

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

{-# 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
Run Code Online (Sandbox Code Playgroud)

那么您的T -> Int -> T运营商是^*,这很简单flip (*^)

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

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

  • @Enlico 然而,值得注意的是,它决不会使您的新类型与现有运算符兼容(导入您的类型的其他人将无法使用“*”;他们还必须隐藏 Prelude 的`*` 并导入您的新运算符)。我以前尝试过这类事情,最终得出的结论是,由于 Prelude 的“*”从根本上来说是与我想要的操作不同的操作,因此实际上最好使用不同的符号。 (2认同)