【问题标题】:Can you overload + in haskell?你可以在haskell中重载+吗?
【发布时间】:2012-01-08 15:08:04
【问题描述】:

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

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

这样的类型
 Pair(2,4) + Pair(1,2) = Pair(3,6)

可以在haskell中做到吗?

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

【问题讨论】:

  • 虽然人们已经回答了如何超载 (+),但您应该使用交互式解释器。您可以尝试执行 (1 + 1) 和 (1.0 + 1.0) 并查看它是否有效。这是一种发现语言的强大方法。
  • Haskell 和 Scala 之间的一个重要区别是重载。当你在 Scala 中做foo + bar 时,它相当于foo.+(bar)。 Scala 是面向对象的,允许同名的方法出现在不同的类中,无论它们的行为是否相似。在 Haskell 中,不同类型的函数只有如果它们是类型类的一部分,则它们可以具有相同的名称。
  • 你是说 1+1Pair(1,2) + Pair(2,3) 不能存在于同一个文件中同一时间?
  • 不,他是说在这两种情况下,+相同的通用 函数——不是两个不同的函数——它是 Num 的一部分类型类。 (+ 有很多不同的实现方式——Num 的每个实例都有一个实现方式。

标签: haskell


【解决方案1】:

是的

(+)Num 类型类的一部分,每个人似乎都觉得你不能为你的类型定义(*) 等,但我强烈反对。

newtype Pair a b = Pair (a,b)  deriving (Eq,Show) 

我认为Pair a b 会更好,或者我们甚至可以直接使用类型(a,b),但是...

这很像两个 Monoid、群、环或数学中的任何东西的笛卡尔积,并且有一种定义数字结构的标准方法,使用起来很明智。

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。这也很好用。

【讨论】:

  • 不,你不需要隐藏任何东西,你不需要导入任何东西。如果您复制代码(两位数据......和实例......),它会按原样工作。它只是添加了标准 Num 类型类的一个简单实例,没有狡猾的技巧。 :)
  • @Priyatham 所以对于那个例子,我会写fromInteger i = ((fromInteger i,0),(0,fromInteger i))。 (第二个和第三个fromInteger 以防您的类型不是((Integer,Integer),(Integer,Integer))。)
  • 不,这是完全自然的。 fromInteger 意味着嵌入同态,{kI|k<-F}F 是同构的。
  • *同构的,不是异构的。呵呵。 02:16 打字不太好!
  • @AntonyHatchkins 对不起,我的错误。将该建议切换为:如果您在两个坐标中使用相同的类型,请执行 newtype Pair a = Pair (a,a) 然后使用 instance Num a => Num (Pair a)
【解决方案2】:

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

【讨论】:

  • 但是为什么人们不使用 + 而不是看起来很奇怪的东西呢?我的意思是它看起来好多了!
  • @hammar (+) 和 abs 有什么区别?两者都属于 Num 类,但我可以在我的模块中覆盖 abs,但不能覆盖 (+)。我也可以覆盖 (++),所以这不是符号问题。这是否意味着类的符号成员具有全局范围?我觉得我错过了什么。
  • @VictorMoroz 不确定您在说什么。您能否向我们展示一些 Haskell 代码,您可以在其中覆盖 abs 但不能覆盖 ++ 不应受到比 abs 更多的特殊待遇。
  • @VictorMoroz:这不是重载,这只是掩盖了名称。是的,你也可以关注(+),符号没有什么特别之处。但是,这可能会非常不方便,因为当您想要引用“真实的”(+) 运算符时,您必须使用限定名称,例如1 Prelude.+ 2.
【解决方案3】:

我将尝试非常直接地回答这个问题,因为您热衷于在重载 (+) 上直接回答“是或否”。答案是肯定的,你可以超载它。有两种方法可以直接重载它,无需任何其他更改,另一种方法是“正确”重载它,这需要为您的数据类型创建一个 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 重新实现为一个库,虽然这是一项艰巨的任务,但不需要编译器黑客攻击。

【讨论】:

  • 谢谢,这是对这个问题最清楚的解释。我喜欢你如何解释由于历史原因所有功能都在 Num 中,以及它应该如何完成。完美。
  • 好的,所以您现在知道答案肯定是,而且实际上有多种方法可以做到这一点。
  • 感谢您的精彩解释,但我想说您帖子的完整答案是“是的,你可以,不,你不应该”。模块类型类不存在此问题,因为它们可以导入限定,但对于 Prelude 将始终存在。不得不承认,有些函数被锁定到某些类型类,如果不这样,生活会更加艰难。即使你定义了 typeclass Multipliable 而不是 Num 它也不能解决向量的问题(有两个产品)。所以+ 属于 Num,如果我的对象不适合 Num 的配置文件,那么我必须创建自己的函数。
  • 遗憾的是 (+) 仅用于数字...一些更明智的运算符组会很好,是的 (+) 在 Addable 类上左右
【解决方案4】:

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)

但是,如果您要为 + 提供一个实例,您还应该为该类型类中的所有其他函数提供一个实例,这可能并不总是有意义。

【讨论】:

  • @drozzy 你可以。为您的自定义数据类型编写一个Num 的实例,然后就可以离开了。
  • @drozzy,对不起,我不小心按了回车键有点太早了:一个正确的答案来了!
  • 那么...为什么我不能编写一个名为“Matrix”或类似名称的新类,并在其上仅定义 (+)、(=) 和 (-)?
  • @drozzy:编译器不会阻止你给出部分类型类定义(但它会产生警告)。问题是其他人可能依赖于完全实现的 Num 和 Eq 类,因此他们的期望与您提供的不匹配。我认为这里真正的问题是 Num 类可能有太多的函数捆绑在一起,不一定相关。
  • 哦,你可以创建一个包含 (+) 的新类型类,但是你会遇到一个模棱两可的问题:Prelude.+ 会与你的 @ 冲突987654334@。您总是可以隐藏 Prelude 版本,但我相信您更愿意保留它,因为它是一个有用的运算符。或者,您可以明确限定您的 Matrix.+,但最好按照 hammar 的建议创建一个新符号,如 |+|
【解决方案5】:

如果你只想要(+) 运算符而不是所有Num 运算符,可能你有一个Monoid 实例,例如Monoid 对的实例是这样的:

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

【讨论】:

  • 问题在于 (+) 运算符与 (+)、(-)、abs 等捆绑在 Num 类型类中,因此如果您想要对于加法语义,Monoid 是实现的最佳类型类。不幸的是,由于历史原因,(+) 并不是 Monoid 加法运算符的名称,它被称为mappend(你可以给它起别名,在这种情况下,yihuang 建议用 (++) 来别名)。这可能是比您所寻找的更具理论性的答案,因为您只是试图重载 (+) 运算符。尽量少争论,这会让人们不想帮忙
  • 好的,谢谢。但是 ++ 不是 +。还是我要瞎了?关于争论 - 我尽量不要。但我一直要一把锤子,人们一直递给我一把螺丝刀。
  • mappend 的标准别名现在是<>,它已经被广泛使用。 @drozzy:我宁愿说,你一直要求mallet,人们会避免给你mallet。问题是,尽管现在在许多编程语言中使用+ 进行任何类型的组合都很常见,但这是一个相当近期的发展,而且,我敢打赌,实际上与某些预定义集合的限制有很大关系的运营商。在数学中,+ 通常受到更多限制,例如假设是可交换的。
猜你喜欢
  • 2013-02-23
  • 2013-09-07
  • 2014-01-02
  • 2021-05-07
  • 1970-01-01
  • 2021-09-18
  • 2022-06-17
  • 2020-01-29
  • 2016-12-01
相关资源
最近更新 更多