【问题标题】:Confused about Haskell polymorphic types对 Haskell 多态类型感到困惑
【发布时间】:2015-05-14 07:04:10
【问题描述】:

我已经定义了一个函数:

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 没有输入为 Int 而b 没有输入为 String ?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    这是一个很常见的误解。

    要理解的关键是,如果你的类型签名中有一个变量,那么调用者可以决定是什么类型,不是你!

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

    您的函数声称能够返回任何可能的类型,但实际上它只返回String。所以它没有做类型签名声称它做的事情。因此,编译时错误。

    很多人显然误解了这一点。在(比如说)Java 中,你可以说“这个函数返回Object”,然后你的函数可以返回它想要的任何东西。所以 function 决定它返回什么类型。在 Haskell 中,调用者 可以决定返回什么类型,而不是函数。

    编辑:请注意,您编写的类型a -> b 是不可能的。没有任何函数可以拥有这种类型。函数不可能凭空构造b 类型的值。唯一可行的方法是,如果某些输入还涉及类型 b,或者 b 属于某种允许值构造的类型类。

    例如:

    head :: [x] -> x
    

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

    同样,gen :: a -> a 是一个完全有效的函数。但它唯一能做的就是原封不动地返回它的输入(即 id 函数所做的)。

    这个类型签名的属性告诉你一个函数做了什么是 Haskell 的一个非常有用和强大的属性。

    【讨论】:

    • Java 中的 Object 类型与 Haskell 中的通用量化类型变量不同。 Object 类型是 Java 的 top type,而通用量化类型变量是 Haskell 中的 bottom type。因此,Java 中的 Object 类型实际上等同于 Haskell 中的 forall r. (forall a. a -> r) -> r,其中 a 是存在量化变量。
    • 糟糕。我误读了你的回答。我以为您是在说 Java 中的 Object 类型等同于 Haskell 中的底部类型。不过,您可以拥有一个在 Haskell 中返回存在类型的函数。因此,从技术上讲,Haskell 中的函数可以选择自己的返回类型。
    • @MathematicalOrchid 那么什么是 'gen' 的有效实现,因为我不能返回除 'b' 以外的任何类型?我看不出这是多态的,即使我知道它是......
    • @blue-sky 有一些方法可以实现gen,但大部分都没用。例如gen x = gen x 有效(并且从未成功产生结果),gen x = unsafeCoerce x 也有效,这实际上可能产生有效结果,但该函数无缘无故地在其名称中没有“不安全”(基本上它就像一个reinterpret_cast 在 C++ 中或在 C 中的 addressof + cast + dereference)。
    • @blue-sky 调用者是调用(应用)函数的表达式。你写这两个表达式的事实是无关紧要的。
    【解决方案2】:

    gen :: a -> b 并不意味着“对于某些类型 a 和某些类型 bfoo 必须是 a -> b 类型”,它的意思是“对于 any 类型 @987654326 @ 和 any 类型为 bfoo 必须是 a -> b 类型。

    激发这个:如果类型检查器看到类似let x :: Int = gen "hello"的东西,它会看到gen在这里被用作String -> Int,然后查看gen的类型,看看它是否可以这样使用.类型是a -> b,可以特化为String -> Int,所以类型检查器认为这没问题并允许这个调用。那是因为函数被声明为类型为a -> b,类型检查器允许你调用你想要的任何类型的函数,并允许你使用任何你想要的类型的结果。

    但是,这显然与您对该函数的定义不符。该函数知道如何将数字作为参数处理 - 仅此而已。同样,它知道如何生成字符串作为结果——仅此而已。很明显,不可能以字符串作为参数调用函数或将函数的结果用作 Int。因此,由于类型 a -> b 允许这样做,因此显然该函数的类型是错误的。

    【讨论】:

      【解决方案3】:

      您的类型签名gen :: a -> b 表明,您的函数可以为 any 类型 a 工作(并为 any 类型 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."a -> b 类型没有有用的函数。
      • 好吧,unsafeCoerce 除外。
      【解决方案4】:

      优秀的答案。但是,鉴于您的个人资料,您似乎了解 Java,因此我认为将其与 Java 联系起来也很有价值。

      Java 提供了两种多态性:

      1. 子类型多态性: 例如,每个类型都是java.lang.Object 的子类型
      2. 泛型多态性:例如,在List<T> 接口中。

      Haskell 的类型变量是 (2) 的一个版本。 Haskell 并没有真正的 (1) 版本。

      考虑泛型多态性的一种方法是根据模板(C++ 人称它们为):具有类型变量参数的类型是可以特化的模板 分为多种单态 类型。例如,接口List<T> 是一个模板,用于构造诸如List<String>List<List<String>> 等单态接口,它们具有相同的结构,但不同之处仅在于类型变量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 -&gt; b 的任何表达式中,类型推断将推断出需要为ab 实例化哪些类型,给定使用gen 的上下文。所以基本上,“调用者选择”意味着任何使用gen 的代码控制ab 将被实例化的类型;如果我写gen [()],那么我隐式地将a 实例化为[()]。这里的错误意味着你的类型声明说gen [()]是允许的,但你的方程gen 2 = "test"暗示它不是。

      【讨论】:

      • +1 谢谢,我有点“明白”,在我看来,一旦使用泛型类型参数,函数的操作就非常有限,因为不能将泛型类型子类型化为更具体行为。我刚刚开始我的 Haskell 之旅,所以希望随着我的深入,我会更有意义。
      【解决方案5】:

      在 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-&gt;b 这样做,你会卡住。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-10-17
        • 1970-01-01
        • 1970-01-01
        • 2012-04-24
        • 1970-01-01
        • 1970-01-01
        • 2017-03-22
        相关资源
        最近更新 更多