虽然我在Haskell示例代码中看到了各种奇怪的东西 - 但我从未见过运算符加上过载。它有什么特别之处吗?
假设我有一个类似 Pair 的类型,我想要像
这样的东西 Pair(2,4) + Pair(1,2) = Pair(3,6)
可以在haskell中做到吗?
我只是好奇,因为我知道Scala可以以一种相当优雅的方式。
答案 0 :(得分:47)
(+)
是Num
类型类的一部分,每个人似乎都觉得你不能为你的类型定义(*)
等,但我强烈不同意。
newtype Pair a b = Pair (a,b) deriving (Eq,Show)
我认为Pair a b
会更好,或者我们甚至可以直接使用(a,b)
类型,但是......
这非常类似于数学中两个Monoids,群,环或其他任何东西的笛卡尔积,并且有一种标准方法可以在其上定义数字结构,这对于使用是明智的。
instance (Num a,Num b) => Num (Pair a b) where
Pair (a,b) + Pair (c,d) = Pair (a+c,b+d)
Pair (a,b) * Pair (c,d) = Pair (a*c,b*d)
Pair (a,b) - Pair (c,d) = Pair (a-c,b-d)
abs (Pair (a,b)) = Pair (abs a, abs b)
signum (Pair (a,b)) = Pair (signum a, signum b)
fromInteger i = Pair (fromInteger i, fromInteger i)
现在我们已经以显而易见的方式重载了(+)
,但也以相同,明显,熟悉的数学方式完成了整个程序并重载(*)
和所有其他Num
函数这对一对。我只是没有看到这个问题。事实上,我认为这是一种很好的做法。
*Main> Pair (3,4.0) + Pair (7, 10.5)
Pair (10,14.5)
*Main> Pair (3,4.0) + 1 -- *
Pair (4,5.0)
*
- 请注意,fromInteger
适用于1
等数字文字,因此在该上下文中将其解释为Pair (1,1.0) :: Pair Integer Double
。这也非常好用。
答案 1 :(得分:31)
Haskell中的重载仅适用于类型类。在这种情况下,(+)
属于Num
类型类,因此您必须为您的类型提供Num
实例。
但是,Num
还包含其他函数,并且行为良好的实例应以一致的方式实现所有这些函数,除非您的类型代表某种数字,否则通常没有意义。
除非是这种情况,否则我建议您定义一个新的运算符。例如,
data Pair a b = Pair a b
deriving Show
infixl 6 |+| -- optional; set same precedence and associativity as +
Pair a b |+| Pair c d = Pair (a+c) (b+d)
然后您可以像任何其他运营商一样使用它:
> Pair 2 4 |+| Pair 1 2
Pair 3 6
答案 2 :(得分:28)
我会尝试非常直接地回答这个问题,因为你热衷于在重载(+)上直接得到“是或否”。答案是肯定的,你可以超载它。有两种方法可以直接重载它,没有任何其他更改,还有一种方法可以“正确”地重载它,这需要为您的数据类型创建一个Num实例。在其他答案中详细阐述了正确的方法,所以我不会过去。
编辑:请注意,我不推荐下面讨论的方式,只记录它。你应该实现Num类型类,而不是我在这里写的任何内容。
重载(+)的第一种(也是最“错误的”)方法是简单地隐藏Prelude。+函数,并定义你自己的名为(+)的函数,该函数对你的数据类型进行操作。
import Prelude hiding ((+)) -- hide the autoimport of +
import qualified Prelude as P -- allow us to refer to Prelude functions with a P prefix
data Pair a = Pair (a,a)
(+) :: Num a => Pair a -> Pair a -> Pair a -- redefinition of (+)
(Pair (a,b)) + (Pair (c,d)) = Pair ((P.+) a c,(P.+) b d ) -- using qualified (+) from Prelude
你可以在这里看到,我们必须通过一些扭曲来隐藏导入的(+)的常规定义,但我们仍然需要一种方法来引用它,因为它是快速添加机器的唯一方法(这是一个原始的操作。)
第二种(稍微错误的)方法是定义你自己的类型类,它只包含你命名的新运算符(+)。你仍然需要隐藏旧的(+)所以haskell不会混淆。
import Prelude hiding ((+))
import qualified Prelude as P
data Pair a = Pair (a,a)
class Addable a where
(+) :: a -> a -> a
instance Num a => Addable (Pair a) where
(Pair (a,b)) + (Pair (c,d)) = Pair ((P.+) a c,(P.+) b d )
这比第一个选项好一点,因为它允许您在代码中使用新的(+)来处理许多不同的数据类型。
但是这些都不推荐,因为正如您所看到的,访问Num类型类中定义的常规(+)运算符非常不方便。即使haskell允许您重新定义(+),所有Prelude和库都期望原始(+)定义。幸运的是,(+)是在类型类中定义的,所以你可以让Pair成为Num的一个实例。这可能是最好的选择,而这正是其他回答者所推荐的。
您遇到的问题是Num类型类中定义的函数可能太多(+是其中之一)。这只是一个历史性的事故,现在Num的使用如此广泛,现在很难改变它。不是将这些功能分解为每个功能的单独类型组(因此它们可以单独覆盖),而是将它们全部组合在一起。理想情况下,Prelude将有一个Addable类型类和一个Subtractable类型类等,允许您一次为一个运算符定义一个实例,而不必实现Num在其中的所有内容。
尽管如此,事实是,如果你想为你的Pair数据类型写一个新的(+),你将会打一场艰苦的战斗。太多其他Haskell代码依赖于Num类型类及其当前定义。
如果您正在寻找Prelude的蓝天重新实现,试图避免当前错误的一些错误,您可以查看Numeric Prelude。你会发现他们已经重新实现了Prelude作为一个库,不需要编译器黑客攻击,尽管这是一项艰巨的任务。
答案 3 :(得分:18)
通过类型类可以在Haskell中进行重载。要获得良好的概述,您可能需要查看this section in Learn You a Haskell。
(+)
运算符是Prelude中Num
类型类的一部分:
class (Eq a, Show a) => Num a where
(+), (*), (-) :: a -> a -> a
negate :: a -> a
...
因此,如果您希望+
的定义适用于对,则必须提供实例。
如果您有类型:
data Pair a = Pair (a, a) deriving (Show, Eq)
然后你可能会有这样的定义:
instance Num a => Num (Pair a) where
Pair (x, y) + Pair (u, v) = Pair (x+u, y+v)
...
将其打入ghci
会给我们:
*Main> Pair (1, 2) + Pair (3, 4)
Pair (4,6)
但是,如果您要为+
提供一个实例,那么您也应该为该类型类中的所有其他函数提供一个实例,这可能并不总是有意义。
答案 4 :(得分:6)
如果您只需要(+)
运算符而不是所有Num
运算符,可能您有一个Monoid
实例,例如Monoid
对的实例是这样的:< / p>
class (Monoid a, Monoid b) => Monoid (a, b) where
mempty = (mempty, mempty)
(a1, b1) `mappend` (a2, b2) = (a1 `mappend` a2, b1 `mappend` b2)
您可以将(++)
设为mappend
的别名,然后就可以编写如下代码:
(1,2) ++ (3,4) == (4,6)
("hel", "wor") ++ ("lo", "ld") == ("hello", "world")