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

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

newtype T = T Int deriving Show

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

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

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

现在让我们假设我希望能够将 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

这显然不起作用,因为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).

回答

非常相似的例子已经经常被问到,答案是这不是一个数字类型,因此不应该有一个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

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

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

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

  • @Enlico 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.

以上是在用户定义的类型和现有类型之间定义已经存在的(例如在Prelude中)运算符的正确方法是什么?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>