Haskell中的return-type-only only多态性好吗?


28

在Haskell中我从未完全了解过的一件事是如何拥有多态常量和函数,这些常量和函数的返回类型不能由其输入类型确定,例如

class Foo a where
    foo::Int -> a

我不喜欢这样的一些原因:

参照透明度:

“在Haskell中,给定相同的输入,一个函数将始终返回相同的输出”,但这是真的吗?read "3"Int上下文中使用时返回3 ,但在上下文中使用时抛出错误(Int,Int)。是的,您可以说这read也是一个类型参数,但是我认为类型参数的隐式性使其失去了某些美感。

单态限制:

关于Haskell的最烦人的事情之一。如果我错了,请纠正我,但是MR的全部原因是看起来共享的计算可能不是因为type参数是隐式的。

输入默认值:

再次是Haskell上最烦人的事情之一。例如,如果您在输出中将函数多态的结果传递给输入中的函数多态,则会发生这种情况。同样,如果我错了,请纠正我,但是如果没有函数的返回类型不能由其输入类型(和多态常量)确定的函数,则不需要这样做。

因此,我的问题是(冒着被盖章为“讨论问题”的风险):是否有可能在类型检查器不允许这些定义的情况下创建类似于Haskell的语言?如果是这样,该限制的利弊是什么?

我可以看到一些直接的问题:

例如,如果2仅具有type Integer2/3则将不再使用当前定义进行类型检查/。但是在这种情况下,我认为具有功能依赖性的类型类可以解决(是的,我知道这是一个扩展)。此外,我认为拥有可以采用不同输入类型的函数比拥有受其输入类型限制的函数要直观得多,但是我们只是将多态值传递给它们。

值的打字喜欢[]Nothing我看来,像一个难啃的骨头。我还没有想到处理它们的好方法。

Answers:


37

实际上,我认为返回类型多态性是类型类的最佳功能之一。在使用了一段时间之后,有时我很难回到没有它的OOP样式建模。

考虑代数的编码。在Haskell中,我们有一个类型类Monoid(忽略mconcat

class Monoid a where
   mempty :: a
   mappend :: a -> a -> a

我们如何将其编码为OO语言的接口?简短的答案是我们不能。这是因为类型mempty(Monoid a) => a亦称,返回类型多态性。具有代数建模的能力对IMO非常有用。*

您从关于“参考透明性”的投诉开始您的帖子。这就提出了一个重要的观点:Haskell是一种面向价值的语言。因此,read 3不必将类似的表达式理解为计算值的事物,也可以将它们理解为值。这意味着真正的问题不是返回类型多态性:它是具有多态类型([]Nothing)的值。如果语言应该具有这些,那么它实际上必须具有多态返回类型才能保持一致。

我们应该说的[]是类型forall a. [a]吗?我认同。这些功能非常有用,并且使语言更加简单。

如果Haskell具有亚型,多态性[]可能是所有人的亚型[a]。问题是,我不知道没有空列表的类型是多态的编码方式。考虑一下如何在Scala中完成此操作(比在规范的静态类型化的OOP语言Java中进行的操作要短)

abstract class List[A]
case class Nil[A] extends List[A]
case class Cons[A](h: A. t: List[A]) extends List[A]

即使在这里,Nil()也是Nil[A]** 类型的对象

返回类型多态的另一个优点是,它使Curry-Howard的嵌入变得更加简单。

考虑以下逻辑定理:

 t1 = forall P. forall Q. P -> P or Q
 t2 = forall P. forall Q. P -> Q or P

我们可以在Haskell中将这些定理简单地捕获为定理:

data Either a b = Left a | Right b
t1 :: a -> Either a b
t1 = Left
t2 :: a -> Either b a
t2 = Right

总结一下:我喜欢返回类型多态性,只有在您对值的概念有限的情况下,才认为它会破坏参照透明性(尽管在特殊类型类情况下这种吸引力不那么明显)。另一方面,我确实找到了您关于MR的观点,并键入了默认强制性。


*。ysdx在评论中指出这并非完全正确:我们可以通过将代数建模为另一种类型来重新实现类型类。像java一样:

abstract class Monoid<M>{
   abstract M empty();
   abstract M append(M m1, M m2);
}

然后,您必须随身传递这种类型的对象。Scala有一个隐式参数的概念,它避免了显式管理这些事物的一些(但据我的经验)并非如此。将实用程序方法(工厂方法,二进制方法等)放在单独的F绑定类型上,事实证明这是一种使用支持​​泛型的OO语言来管理事物的非常好方法。就是说,如果我没有使用类型类对事物进行建模的经验,我不确定是否会搞怪这种模式,而且我不确定其他人也会这样做。

它也有局限性,开箱即用,无法获得实现任意类型的typeclass的对象。您必须显式地传递值,使用Scala的隐式值,或使用某种形式的依赖项注入技术。生活变得丑陋。另一方面,很高兴您可以为同一个类型具有多个实现。事物可以多种方式成为Monoid。此外,将这些结构分开携带会给IMO带来数学上更现代,更具建设性的感觉。因此,尽管我通常仍然更喜欢Haskell的方式,但我可能夸大了自己的论点。

具有返回类型多态性的类型类使这种事情易于处理。那不是最好的方法。

**。 约尔格·米塔格(JörgW Mittag)指出,这并不是在Scala中做到这一点的典型方法。相反,我们将使用类似以下内容的标准库:

abstract class List[+A] ...  
case class Cons[A](head: A, tail: List[A]) extends List[A] ...
case object Nil extends List[Nothing] ...

这利用了Scala对底部类型以及协变量类型参数的支持。因此,Nil类型Nil不是Nil[A]。在这一点上,我们离Haskell还很远,但是有趣的是注意到Haskell如何表示底部类型

undefined :: forall a. a

也就是说,它不是所有类型的子类型,而是所有类型的成员的多态(sp)。
还有更多的返回类型多态性。


4
“我们如何将其编码为OO语言的接口?” 您可以使用一流的代数:接口Monoid <X> {X empty(); X append(X,X); }但是,您需要显式地传递它(这是在Haskell / GHC幕后完成的)。
ysdx 2011年

@ysdx没错。在支持隐式参数的语言中,您会得到与Haskell的类型类非常接近的东西(例如Scala中)。我意识到这一点。不过,我的观点是,这使生活变得非常困难:我发现自己不得不使用在各处存储“类型类”的容器(糟糕!)。不过,我的回答可能应该不太夸张。
菲利普·JF

+1,很棒的答案。不过,一个不相关的nitpick:Nil应该是case object,而不是case class
约尔格W¯¯米塔格

@JörgW Mittag并非完全无关。我已进行修改以处理您的评论。
菲利普·JF

1
谢谢您的答复。我可能需要一些时间来消化/理解它。
dainichi 2011年

12

只是要注意一个误解:

“在Haskell中,给定相同的输入,一个函数将始终返回相同的输出”,但这是真的吗?在Int上下文中使用时,读为“ 3”时返回3,但在(Int,Int)上下文中使用时抛出错误。

仅仅因为我的妻子开着斯巴鲁而我开着斯巴鲁,并不意味着我们开着同一辆车。仅仅因为命名了2个函数read并不意味着它是相同的函数。实际上read :: String -> Int是在Int的Read实例中read :: String (Int, Int)定义的,而在(Int,Int)的Read实例中定义的是。因此,它们是完全不同的功能。

这种现象在编程语言中很常见,通常称为重载


6
我认为您有点漏掉了问题的重点。在大多数具有即席多态性(也称为重载)的语言中,选择要调用的函数仅取决于参数类型,而不取决于返回类型。这使得更容易推断函数调用的含义:只需从表达式树的叶子开始,然后以“向上”的方式工作。在Haskell(以及少数其他支持返回类型重载的语言)中,您可能必须立即考虑整个表达式树,甚至要弄清楚一个小子表达式的含义。
劳伦斯·贡萨尔维斯

1
我认为您完美地解决了问题的关键。甚至莎士比亚也说过:“任何其他名称的功能也将起作用。” +1
托马斯·爱丁

@Laurence Gonsalves-Haskell中的类型推断不是参照透明的。代码的含义可能取决于使用类型的上下文,因为类型推断会将信息向内拉。这不仅限于返回类型的问题。Haskell有效地将Prolog内置到其类型系统中。这可以使代码不太清晰,但也具有很大的优势。我个人认为,最重要的参照透明性是运行时发生的事情,因为如果没有它,懒惰将无法解决。
Steve314

@ Steve314我想我还没有看到这样的情况,即缺少参照透明类型的推断不会使代码变得不太清楚。要推理出任何复杂的事物,就需要能够从心理上“块化”事物。如果你告诉我你有一只猫,我不会想到数十亿个原子或单个细胞的云。同样,在阅读代码时,我将表达式划分为其子表达式。Haskell通过两种方式克服了这一点:“错误”类型推断和过于复杂的运算符重载。Haskell社区还讨厌parens,从而使parpare更糟。
劳伦斯·贡萨尔维斯

1
@LaurenceGonsalves是正确的,infix可以滥用此功能。但这是用户的失败。像Java中一样,OTOH是限制性的,恕我直言,这不是正确的方法。要看到这一点,只需要处理一些与BigIntegers相关的代码即可。
Ingo

7

我真的希望永远不要创建“返回类型多态”一词。这会引起对正在发生的事情的误解。可以说,消除“返回类型多态性”将是极其临时性和表现力的致命改变,但它甚至无法远程解决问题中提出的问题。

返回类型绝非特别。考虑:

class Foo a where
    foo :: Maybe a -> Bool

x = foo Nothing

此代码会导致所有与“返回类型多态性”相同的问题,并且也演示与OOP相同的差异。但是,没有人谈论“第一个参数可能是类型多态性”。

关键思想是实现使用类型来解析要使用的实例。任何类型的(运行时)值都与它无关。实际上,即使对于没有值的类型也可以使用。特别是,Haskell程序没有它们的类型就没有意义。(具有讽刺意味的是,这使Haskell成为教堂风格的语言,而不是咖喱风格的语言。我的博客文章对此进行了详细阐述。)


“此代码导致所有与“返回类型多态性”相同的问题”。不,不是。我可以查看“ foo Nothing”并确定其类型。这是一个布尔。无需查看上下文。
dainichi

4
实际上,代码不会进行类型检查,因为编译器不知道a“返回类型”情况下的情况。同样,返回类型没有什么特别的。我们需要知道所有子表达式的类型。考虑一下let x = Nothing in if foo x then fromJust x else error "No foo"
Derek Elkins

2
更不用说“第二个论点多态性”;像这样的函数Int -> a -> Bool实际上是通过遍历Int -> (a -> Bool)返回值中的多态来实现的。如果允许它在任何地方,那么它必须无处不在。
Ryan Reich

4

关于您关于多态值的参照透明性的问题,这可能会有所帮助。

考虑这个名字 1。它通常表示不同(但固定)的对象:

  • 1 如整数
  • 1 如真实
  • 1 就像在平方单位矩阵中一样
  • 1 如身份功能

在此重要的是要注意,在每种情况下1都是固定值。换句话说,每个名称-上下文对表示一个唯一的对象。没有上下文,我们将无法知道所指的是哪个对象。因此,必须推断(如果可能)或显式提供上下文。除方便的符号外,一个不错的好处是能够在通用上下文中表达代码。

但是,由于这只是表示法,因此我们可以改用以下表示法:

  • integer1 如整数
  • real1 如真实
  • matrixIdentity1 就像在平方单位矩阵中一样
  • functionIdentity1 如身份功能

在这里,我们获得了明确的名称。这使我们仅从名称即可派生上下文。因此,仅需要对象的名称即可完全表示一个对象。

Haskell类型类选择了前一种表示法。现在这是隧道尽头的灯:

read是一个名称,而不是一个值(它没有上下文),但是read :: String -> a是一个值(它既有名称又有上下文)。因此,函数read :: String -> Intread :: String -> (Int, Int)实际上是不同的函数。因此,如果他们不同意他们的输入,则引用透明性不会被破坏。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.