你可以在haskell超载吗?

时间:2011-11-29 08:52:40

标签: haskell

虽然我在Haskell示例代码中看到了各种奇怪的东西 - 但我从未见过运算符加上过载。它有什么特别之处吗?

假设我有一个类似 Pair 的类型,我想要像

这样的东西
 Pair(2,4) + Pair(1,2) = Pair(3,6)

可以在haskell中做到吗?

我只是好奇,因为我知道Scala可以以一种相当优雅的方式。

5 个答案:

答案 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

(+)运算符是PreludeNum类型类的一部分:

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")