23

这可能是一个非常基本的问题,但是......

一个定义为的函数,比如说

foo :: a -> Integer

表示从任何类型到整数的函数。如果是这样,那么理论上应该能够为任何类型定义它,就像这样

foo 1 = 10
foo 5.3 = 100
foo (x:xs) = -1
foo  _     = 0

但是 Haskell 只允许一个通用的定义,比如foo a = 0.

即使您限制a为某一类类型之一,例如 Show typeclass 的实例:

foo :: (Show a) => a -> Integer

你仍然不能做类似的事情

foo "hello" = 10
foo   _     = 0

即使"hello" :: [Char]是一个实例Show

为什么会有这样的限制?

4

5 回答 5

32

这是一个特性,实际上是非常基本的。它归结为编程语言理论中称为参数的属性。粗略地说,这意味着评估永远不应该依赖于编译时变量的类型。您不能静态地查看不知道其具体类型的值。

为什么这么好?它提供了关于程序的更强的不变量。例如,您仅从类型就知道a -> a必须是恒等函数(或发散)。类似的“自由定理”适用于许多其他多态函数。参数化也是更高级的基于类型的抽象技术的基础。例如,ST s aHaskell 中的类型(状态单子),以及相应runST函数的类型,都依赖于s参数化。这确保了正在运行的函数不会弄乱状态的内部表示。

这对于有效实施也很重要。程序不必在运行时传递昂贵的类型信息(类型擦除),编译器可以为不同的类型选择重叠的表示。作为后者的一个例子,0 和 False 以及 () 和 [] 在运行时都用 0 表示。如果允许像您这样的功能,这是不可能的。

于 2012-05-30T09:52:27.637 回答
21

Haskell 喜欢一种称为“类型擦除”的实现策略:类型没有计算意义,因此您发出的代码不需要跟踪它们。这对性能来说是一个重大的好处。

你为这种性能优势付出的代价是类型没有计算意义:一个函数不能根据它传递的参数的类型来改变它的行为。如果你允许类似的东西

f () = "foo"
f [] = "bar"

那么该属性将不成立: 的行为f确实取决于其第一个参数的类型。

肯定有语言允许这种事情,特别是在依赖类型的语言中,类型通常无论如何都不能被删除。

于 2012-05-30T07:36:45.200 回答
20

对于一个函数a -> Integer,只有一种行为是允许的——返回一个常量整数。为什么?因为你不知道什么是类型a。在没有指定约束的情况下,它绝对可以是任何东西,并且因为 Haskell 是静态类型的,所以您需要在编译时解决与类型有关的所有问题。在运行时,类型信息不再存在,因此无法查询 - 所有要使用的函数的选择都已做出。

最接近 Haskell 允许这种行为的是类型类的使用——如果你创建了一个Foo用一个函数调用的类型类:

class Foo a where
    foo :: a -> Integer

然后你可以为不同类型定义它的实例

instance Foo [a] where
    foo [] = 0
    foo (x:xs) = 1 + foo xs

instance Foo Float where
    foo 5.2 = 10
    foo _ = 100

那么只要你能保证某个参数x是一个Foo你就可以调用foo它。不过你仍然需要它——你不能再写一个函数

bar :: a -> Integer
bar x = 1 + foo x

因为编译器不知道这aFoo. 您必须告诉它,或者省略类型签名并让它自己弄清楚。

bar :: Foo a => a -> Integer
bar x = 1 + foo x

Haskell 只能使用编译器所拥有的关于某事物类型的尽可能多的信息进行操作。这听起来可能有限制,但实际上类型类和参数多态性是如此强大,我从不错过动态类型。事实上,我通常觉得动态类型很烦人,因为我从来不完全确定任何东西到底是什么。

于 2012-05-30T08:05:16.980 回答
16

正如您所描述的那样,该类型a -> Integer并不真正意味着“任何类型的函数”。Integer当定义或表达式具有类型a -> Integer时,这意味着对于任何类型T,都可以将此定义或表达式特化或实例化为类型函数T -> Integer

稍微切换符号,一种思考方式是它foo :: forall a. a -> Integer实际上是两个参数的函数:一个类型a和该类型的值a。或者,就柯里化而言,foo :: forall a. a -> Integer它是一个将类型T作为参数的函数,并为此生成一个特殊类型T -> Integer的函数T。以恒等函数为例(产生其参数作为结果的函数),我们可以如下演示:

-- | The polymorphic identity function (not valid Haskell!)
id :: forall a. a -> a
id = \t -> \(x :: t) -> x

这种将多态性实现为多态函数的类型参数的想法来自一个名为System F的数学框架,Haskell 实际上将其用作其理论来源之一。然而,Haskell 完全隐藏了将类型参数作为参数传递给函数的想法。

于 2012-05-30T18:52:47.507 回答
12

这个问题是基于一个错误的前提,Haskell 可以做到!(虽然它通常只在非常特殊的情况下使用)

{-# LANGUAGE ScopedTypeVariables, NoMonomorphismRestriction #-}

import Data.Generics

q1 :: Typeable a => a -> Int
q1 = mkQ 0 (\s -> if s == "aString" then 100 else 0)

q2 :: Typeable a => a -> Int
q2 = extQ q1 (\(f :: Float) -> round f)

加载并进行试验:

Prelude Data.Generics> q2 "foo"
0
Prelude Data.Generics> q2 "aString"
100
Prelude Data.Generics> q2 (10.1 :: Float)
10

这不一定与声称类型没有计算意义的答案相冲突。这是因为这些示例需要Typeable约束,它将类型具体化为在运行时可访问的数据值。

大多数所谓的泛型函数(例如 SYB)依赖于一个Typeable或一个Data约束。一些软件包引入了它们自己的替代功能,以达到基本相同的目的。如果没有类似这些类的东西,就不可能做到这一点。

于 2012-05-30T09:55:25.270 回答