在用户定义类型和现有类型之间定义已经存在的(例如在 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
中)。
我该如何处理?
至于为什么会出现这个问题,我可以设如下
- 在我的程序中,我想对笛卡尔平面中的二维向量使用
(Int,Int)
,
- 但我也用
(Int,Int)
来做另一个不相关的事情,
- 因此我必须通过对其中至少一个使用
newtype
来消除两者之间的歧义,或者,如果出于其他几个原因使用 (Int,Int)
,那么为什么不将它们全部设为 newtype
s 换行 (Int,Int)
?
- 由于
newtype Vec2D = Vec2D (Int,Int)
表示平面中的矢量,因此能够做到 Vec2D (2,3) * 4 == Vec2D (8,12)
. 是有意义的
已经经常询问非常相似的例子,答案是这 不是 数字类型,因此不应该有 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
语义但类型实际上无法提供时,它会在运行时可怕地中断那。
假设我有一个包装现有类型的自定义类型,
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
中)。
我该如何处理?
至于为什么会出现这个问题,我可以设如下
- 在我的程序中,我想对笛卡尔平面中的二维向量使用
(Int,Int)
, - 但我也用
(Int,Int)
来做另一个不相关的事情, - 因此我必须通过对其中至少一个使用
newtype
来消除两者之间的歧义,或者,如果出于其他几个原因使用(Int,Int)
,那么为什么不将它们全部设为newtype
s 换行(Int,Int)
? - 由于
newtype Vec2D = Vec2D (Int,Int)
表示平面中的矢量,因此能够做到Vec2D (2,3) * 4 == Vec2D (8,12)
. 是有意义的
已经经常询问非常相似的例子,答案是这 不是 数字类型,因此不应该有 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
语义但类型实际上无法提供时,它会在运行时可怕地中断那。