【问题标题】:Why does Haskell hide functions with the same name but different type signatures?为什么 Haskell 会隐藏同名但类型签名不同的函数?
【发布时间】:2013-02-06 09:58:31
【问题描述】:

假设我要在字符串上定义(+),而不是通过给出Num String 的实例。

为什么 Haskell 现在隐藏了 Nums (+) 函数?毕竟我提供的功能:

(+) :: String -> String -> String

可以通过编译器与 Prelude 的(+) 区分开来。为什么两个函数不能存在于同一个命名空间中,但具有不同的、不重叠的类型签名?

只要代码中没有调用函数,Haskell 就会关心是否存在歧义。然后使用参数调用函数将确定类型,以便选择适当的实现。

当然,一旦有实例Num String,实际上就会发生冲突,因为此时Haskell无法根据参数类型决定选择哪个实现,如果函数被实际调用。
在这种情况下,应该引发错误。

这不允许函数重载而没有陷阱/歧义吗?

注意:我不是在谈论动态绑定。

【问题讨论】:

  • 正如其他人所说,Haskell 仅通过类型类提供重载。这不是设计语言的唯一方法,但它有一个很好的属性,您可以实际为程序片段赋予类型,例如,(+) 具有类型(Num a) => a -> a -> a。有了你的建议,(+) 就没有任何类型了。
  • 那么这将不是一个 Hindley-Milner 类型系统。类型推断会更复杂。 (+) 的类型必须是真正的联合,"''true union'' types are not present in Haskell"。如果允许,一些拼写错误将被接受为类型良好的程序。 tagged union types 没有歧义(即当我们明确地是 forced to use type constructors 时)。
  • @WillNess 是否有充分的理由说明“Hindley-Milner 类型系统”应该比他提出的更好?它有什么优点?该提案的缺点是什么?
  • @Odin 也许它的类型推断算法更简单。

标签: haskell types language-design


【解决方案1】:

Haskell 根本不支持函数重载(通过类型类除外)。原因之一是函数重载不适用于类型推断。如果你有像f x y = x + y 这样的代码,Haskell 怎么知道xy 是Nums 还是Strings,即f 的类型应该是f :: Num a => a -> a -> a 还是f :: String -> String -> String

PS:这与您的问题并不真正相关,但是如果您假设一个开放的世界,那么这些类型并不是严格不重叠的,即在某个模块中的某个地方可能有一个 Num String 的实例,当导入,会破坏你的代码。因此,Haskell 永远不会根据给定类型没有给定类型类的实例这一事实做出任何决定。当然,即使不涉及类型类,函数定义也会隐藏具有相同名称的其他函数定义,所以正如我所说:与您的问题并不真正相关。


关于为什么必须在定义站点知道函数的类型而不是在调用站点推断:首先,函数的调用站点可能与函数定义位于不同的模块中(或在多个不同的模块中),所以如果我们必须查看调用站点来推断函数的类型,我们就必须跨模块边界执行类型检查。也就是说,在对模块进行类型检查时,我们还必须遍历导入该模块的所有模块,因此在最坏的情况下,每次更改单个模块时我们都必须重新编译所有模块。这将极大地复杂化并减慢编译过程。更重要的是,它会使库无法编译,因为库的本质是它们的函数将被编译器在编译库时无法访问的其他代码库使用。

【讨论】:

  • 嗯,但是在您为 Num String 引入实例的那一刻,它们确实重叠,之前它们不重叠。为什么 Haskell 在实际出现问题时不能抱怨,而不是“过于悲观”?至于您的第一个问题:为什么 Haskell 必须 关心?在你调用函数之前,你知道参数的类型,它不需要知道。当你调用它时,类型将被确定。
  • @phant0m 你想到的是一种运行时的动态调度。但是类型检查(可能告诉你)发生在编译时。
  • @phant0m 至少隐含地暗示了这一点。你说函数被调用时,我认为这可能是一种草率的说法:当编译器看到函数应用程序时。如果是这个意思,那么请给出f x y = x + x + y + y的类型
  • @phant0m 因此,您承认您不能键入只有这个定义(当然还有重载 (+) 的定义)的模块,而没有别的?所以这就是为什么 Haskell 这样做的原因。
  • @phant0m 您的提议需要跨模块边界进行类型检查,并且无法对库进行类型检查(因为在您的提案中,只有访问调用站点才能进行类型检查并且在编写库时没有调用站点)。
【解决方案2】:

只要函数没有被调用

在某些时候,使用该功能

不不不。在 Haskell 中,您不会想到“之前”或“您所做的那一刻......”,而是一劳永逸地定义事物。这在变量的运行时行为中最为明显,但也转化为函数签名和类实例。这样,您不必对编译顺序进行所有繁琐的思考,并且可以避免多种方式,例如由于程序中的一个微小变化,C++ 模板/重载经常会严重崩溃。

另外,我认为您不太了解 Hindley-Milner 的工作原理。

在你调用函数之前,此时你知道参数的类型,它不需要知道。

好吧,你通常知道参数的类型!有时可能会显式给出,但通常是从另一个参数或返回类型推导出来的。例如,在

map (+3) [5,6,7]

编译器不知道数字文字的类型,它只知道它们是数字。这样,您可以根据自己的喜好评估结果,这允许您在其他语言中梦寐以求的东西,例如符号类型,其中

> map (+3) [5,6,7] :: SymbolicNum
[SymbolicPlus 5 3, SymbolicPlus 6 3, SymbolicPlus 7 3]

【讨论】:

  • @phant0m 因为编译时类型检查避免了大量令人讨厌和难以调试的运行时错误,并允许通过专门化进行优化。后期绑定并不是您认为的性能和正确性中立的灵丹妙药。
  • 对不起 - 我误解了你的短语“在某些时候,使用函数时,参数的类型将被确定”。我从您的回复中意识到您的意思是“在代码中的某个点,使用函数的地方,将确定参数的类型”。听起来确实有点像您想要鸭子打字或至少完全取消课程。为什么无论如何都需要调用您的函数(+)?当然,任何阅读您的代码的人都会感到非常困惑。我认为 VBA 可以做到这一点,而且我认为它很丑。
  • @AndrewC:实际上,许多语言都允许将字符串与+ 连接起来(我马上就能想到Javascript、C++、Python,甚至Java),所以很多程序员实际上可能不会太困惑;但我也不喜欢这样。毕竟,您希望加法是一个组操作/至少有一个部分逆,不是吗?字符串连接只是一个幺半群操作,因此 Haskell 的 <>(或简称为 ++!)对此更自然。
  • @AndrewC 抱歉,在这里我不得不在 cmets 中解释了好几次之后,我意识到这很令人困惑,但我不知道有什么更好的表达方式。我会从你那里偷那个:)
  • @leftaroundabout 是的。他们为 (+) 做了一个特殊的魔法案例。我的意思是它通常会让人很困惑。 fmap 的使用是否意味着有一个 Functor,或者只是您碰巧使用了名称 fmap 而没有创建 Functor 实例?使用>> 是否意味着我在一个单子中,或者只是因为我喜欢这个形状并喜欢将它用于flip (.)?呸呸呸! !?*!
猜你喜欢
  • 1970-01-01
  • 2010-10-27
  • 1970-01-01
  • 2011-07-19
相关资源
最近更新 更多