混淆的Haskell多态类型

4

我已经定义了一个函数:

gen :: a -> b

所以只是试图提供一个简单的实现:

gen 2 = "test"
但是会抛出错误:
gen.hs:51:9:
    Couldn't match expected type ‘b’ with actual type ‘[Char]’
      ‘b’ is a rigid type variable bound by
          the type signature for gen :: a -> b at gen.hs:50:8
    Relevant bindings include gen :: a -> b (bound at gen.hs:51:1)
    In the expression: "test"
    In an equation for ‘gen’: gen 2 = "test"
Failed, modules loaded: none.
所以我的函数不正确。为什么a没有被定义为整数类型,而b没有被定义为字符串类型?
5个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
11

这是一个非常常见的误解。

关键是要理解,如果你在类型签名中有一个变量,那么调用方决定它的类型,而不是你!

因此,你不能说“这个函数返回类型x”,然后只返回一个String;你的函数实际上必须能够返回调用者可能要求的任何可能类型。如果我要求你的函数返回一个Int,它就必须返回一个Int。如果我要求它返回一个Bool,它就必须返回一个Bool

你的函数声称能够返回任何可能类型,但实际上它只返回String。所以它不符合类型签名所声称的功能。因此,在编译时会出现错误。

很多人显然对此存在误解。例如在Java中,你可以说“这个函数返回Object”,然后你的函数可以返回任何你想要的东西。因此,函数决定它返回的类型。但在Haskell中,调用方决定返回什么类型,而不是函数。

编辑:注意,你写的类型a -> b是不可能的。任何函数都永远不可能拥有这个类型。没有办法让一个函数从虚无中构造出一个类型为b的值。唯一的方法就是,某些输入也涉及到类型b,或者b属于某种允许值构造的类型类。

例如:

head :: [x] -> x

这里的返回类型是x("任何可能的类型"),但输入类型也提到了x,所以这个函数是可行的;你只需要返回原始列表中的一个值。

同样,gen :: a -> a 是一个完全有效的函数。但它唯一能做的就是返回其输入的内容而不做改变(即id函数的功能)。

类型签名告诉你一个函数的作用,这是Haskell非常有用和强大的特性。


Java中的“Object”类型与Haskell中的全量化类型变量不同。 “Object”类型是Java中的顶级类型,而全量化类型变量是Haskell中的底部类型。因此,Java中的“Object”类型实际上等同于Haskell中的forall r. (forall a. a -> r) -> r,其中a是存在量化变量。 - Aadit M Shah
糟糕,我误读了你的回答。我以为你是说Java中的“Object”类型等同于Haskell中的底部类型。尽管如此,在Haskell中可以有一个返回存在类型的函数。因此,从技术上讲,Haskell中的函数可以选择自己的返回类型。 - Aadit M Shah
2
@blue-sky 有一些实现 gen 的方法,但大多数都是无用的。例如 gen x = gen x 可以工作(但从未成功产生结果),gen x = unsafeCoerce x 也可以工作,它实际上可能会产生有效的结果,但这个函数之所以没有 "unsafe" 在其名称中是有原因的(基本上它就像 C++ 中的 reinterpret_cast 或者 C 中的 addressof + cast + dereference)。 - sepp2k
@MathematicalOrchid “调用者决定类型,而不是你!”在这个上下文中,“调用者”是什么?调用者不就是调用函数的代码,也就是我吗? - blue-sky
1
@blue-sky 调用者是调用(应用)函数的表达式。你编写这两个表达式的事实是无关紧要的。 - Rein Henrichs
显示剩余4条评论

7
gen :: a -> b并不是意味着"对于某些类型abfoo必须是类型a -> b",而是意味着"对于任何类型a任何类型bfoo必须是类型a -> b"。 为了解释这一点:如果类型检查器看到类似let x :: Int = gen "hello"的东西,它会发现gen在这里被用作String -> Int,然后查看gen的类型以确定它是否可以这样使用。该类型是a -> b,可以特化为String -> Int,因此类型检查器决定这是可以的,并允许此调用。这是因为函数声明为具有类型a -> b,类型检查器允许您使用任何想要的类型调用函数,并允许您将结果用作任何想要的类型。 然而,这显然与您给出的函数定义不符。该函数知道如何处理数字作为参数-没有别的。同样,它知道如何生成字符串作为其结果-没有别的。因此,显然不应该能够使用字符串作为其参数调用函数或将函数的结果用作Int。因此,由于类型a -> b会允许这样做,它显然是该函数的错误类型。

2
您的类型签名gen :: a -> b表明,您的函数可以适用于任何类型a(并提供调用者所需的任何类型b)。 除了这样的函数很难得到之外,行gen 2 = "test"试图返回一个String,这很可能不是调用者所需的。

“很难得到”是什么意思?通常情况下,创建类型为 a -> b 的函数是不可能的。类型为 a -> b 的唯一函数是 let f x = f x 或类似于 let f x = undefinedlet f x = error "This function is pointless." 等变体。这种类型的函数没有任何用处。 - Aadit M Shah
1
除了 unsafeCoerce - shang

1
优秀的答案。然而,根据您的个人资料,您似乎了解Java,因此我认为将其与Java联系起来也很有价值。 Java提供了两种多态性: 1.子类型多态性:例如,每种类型都是java.lang.Object的子类型。 2.通用多态性:例如,在List接口中。 Haskell的类型变量是(2)的一种版本。Haskell实际上没有(1)的版本。 思考通用多态性的一种方式是使用模板(这是C++人们称之为模板的东西):具有类型变量参数的类型是可以特化为各种单态类型的模板。例如,接口List是用于构造类似List、List>等单态接口的模板,它们具有相同的结构,但仅因实例化类型统一替换签名中的类型变量T而不同。 “调用者选择”的概念,正如其他几位回答者所提到的,基本上是指实例化的友好方式。例如,在Java中,类型变量最常被“选择”的时间点是在对象实例化时:
List<String> myList = new ArrayList<String>();
第二个共同点是泛型类型的子类型可以实例化超类型的变量:
class MyFunction implements Function<Integer, String> {
    public String apply(Integer i) { ... }
}

第三种方法是允许调用者实例化一个不属于其封闭类型参数的变量的方法:

/**
 * Visitor-pattern style interface for a simple arithmetical language
 * abstract syntax tree.
 */
interface Expression {
    // The caller of `accept` implicitly chooses which type `R` is,
    // by supplying a `Visitor<R>` with `R` instantiated to something
    // of its choice.
    <R> accept(Expression.Visitor<R> visitor);

    static interface Visitor<R> {
        R constant(int i);
        R add(Expression a, Expression b);
        R multiply(Expression a, Expression b);
    }
}
在Haskell中,实例化是通过类型推断算法隐式进行的。在任何使用gen :: a -> b的表达式中,类型推断将推断出需要为ab实例化哪些类型,给定gen所用的上下文。因此,基本上,“调用者选择”意味着使用gen的任何代码都控制了要将ab实例化到哪些类型;如果我编写gen [()],那么我就隐式地将a实例化为[()]。这里的错误意味着你的类型声明说允许gen [()],但你的方程gen 2 = "test"暗示它不允许。

1
+1 谢谢,我有点“明白了”,在我看来,一旦使用了通用类型参数,函数的操作就非常有限,因为无法将通用类型子类型化为更具体的行为。我刚开始学习 Haskell,希望随着深入学习会更加清晰明了。 - blue-sky

0
在Haskell中,类型变量是隐式量化的,但我们可以将其显式化:
{-# LANGUAGE ScopedTypeVariables #-}

gen :: forall a b . a -> b
gen x = ????

"forall"实际上只是一个类型级别的lambda,通常写作Λ。因此,gen是一个接受三个参数的函数:一个类型,绑定到名称a,另一个类型,绑定到名称b,以及一个类型为a的值,绑定到名称x。当调用您的函数时,它将使用这三个参数进行调用。考虑一个更合理的情况:

fst :: (a,b) -> a
fst (x1,x2) = x1

这将被翻译成

fst :: forall (a::*) (b::*) . (a,b) -> a
fst = /\ (a::*) -> /\ (b::*) -> \ (x::(a,b)) ->
   case x of
      (x1, x2) -> x1

* 是正常具体类型的类型(通常称为“种类”)。如果我调用 fst (3::Int, 'x'),那么它会被翻译成

fst Int Char (3Int, 'x')
在编程中,我使用3Int来特别表示Int版本的3。然后我们可以按照以下方式计算:
fst Int Char (3Int, 'x')
=
(/\ (a::*) -> /\ (b::*) -> \(x::(a,b)) -> case x of (x1,x2) -> x1) Int Char (3Int, 'x')
=
(/\ (b::*) -> \(x::(Int,b)) -> case x of (x1,x2) -> x1) Char (3Int, 'x')
=
(\(x::(Int,Char)) -> case x of (x1,x2) -> x1) (3Int, x)
=
case (3Int,x) of (x1,x2) -> x1
=
3Int
无论我传入什么类型,只要我传入的值匹配,fst函数就能产生所需类型的结果。如果你尝试对a->b这样做,你会陷入困境。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,