【问题标题】:compiler error by function application函数应用程序的编译器错误
【发布时间】:2017-02-18 22:42:29
【问题描述】:

我正在学习haskell,但无法配置它,为什么以下代码sn-p无法编译:

*Uncurry> applyTwice f x = f f x

<interactive>:14:20: error:
    • Occurs check: cannot construct the infinite type:
        t ~ t -> t2 -> t1
    • In the first argument of ‘f’, namely ‘f’
      In the expression: f f x
      In an equation for ‘applyTwice’: applyTwice f x = f f x
    • Relevant bindings include
        x :: t2 (bound at <interactive>:14:14)
        f :: t -> t2 -> t1 (bound at <interactive>:14:12)
        applyTwice :: (t -> t2 -> t1) -> t2 -> t1
          (bound at <interactive>:14:1)

这样就好了:

applyTwice f x = f (f x)

在haskell函数应用程序是左关联的,第一个代码sn-p会像这样应用:

(f f) x

为什么(f f) x错了?

【问题讨论】:

  • 不清楚你在问什么。你的问题是“为什么f f 是错的?”是“为什么f f x 表示(f f) x 而不是f (f x)”?还有什么?
  • f 的类型必须是 ((((…) -&gt; a -&gt; b) -&gt; a -&gt; b) -&gt; a -&gt; b) -&gt; a -&gt; b
  • @n.m 我的意思是为什么f f x 是错误的。
  • 如果f f x 有意义,即(f f) x 有意义,那么f f 本身意味着什么?如果例如f :: Int -&gt; Int,那么f 42 是一个整数。但是,f 不是整数(它是一个函数),所以我不能将它作为f 本身的参数传递。
  • @chi 所以不像是高阶函数?

标签: haskell


【解决方案1】:

如果f f 有效,f 的类型应该是什么?

我们将f 应用于一个参数,所以f 必须是一个函数:f :: a -&gt; b 对于某些类型ab

我们应用函数的参数是f,所以它的类型必须是af :: a(即我们有(f :: a -&gt; b) (f :: a))。

因为这两个是相同的f,所以我们得到a -&gt; b = a。如果aa -&gt; b相同,那么我们可以将a代入,a -&gt; b(a -&gt; b) -&gt; b相同,即与((a -&gt; b) -&gt; b) -&gt; b相同等

这种扩展永远不会结束,这就是 ghc 抱怨 f 具有“无限类型”的原因。

【讨论】:

  • 你能用 lambda 演算解释一下吗?
  • @zero_coding 普通的 lambda 演算没有类型系统,那它是如何工作的呢?
  • @zero_coding: 看看我的(长)帖子标题为“查看类型”的部分,有一步一步的解释,让我引用自己的话:如果我们真的使用 f f x,则 f 的类型必须满足以下要求: f 必须匹配 applyTwice 的类型签名中指定的函数类型 a -> a 在 f f f 应用于自身,这意味着 a -> a 中的 a 必须是等于a -> a,这是不可能的!
  • @melpomene 为什么第一次替换后没有完成(a -&gt; b) -&gt; b
  • @zero_coding 因为那里还有一个a。方程现在是a = (a -&gt; b) -&gt; b,它正在尝试求解a
【解决方案2】:

f f x 表示您想将fx 传递给f

您需要确保首先评估f x,以便只有一个参数传递给最左边的f

您可以按照您的发现将其用括号括起来,或者使用应用程序运算符(在这种情况下两者实际上是等效的):

f (f x) 
f $ f x

【讨论】:

    【解决方案3】:

    为什么 (f f) x 是错的?

    好问题,我不认为它是完全错的,我稍后会尝试拯救它!

    但是,我想先解释一些基本概念。

    我知道任务是创建一个函数applyTwice,将赋予它的任何函数应用于第二个参数,然后再次应用于结果,对吗?

    如果您真正考虑提供给applyTwice 的函数的具体示例,您可能会注意到,并非所有函数都可以应用两次,例如,将Bool 转换为的函数String:

    boolToString :: Bool -> String
    boolToString True = "yes!!1eleven"
    boolToString False = "ohnoes"
    

    只能只应用一次:在将boolToString 应用到Bool 之后,我们会得到String,我们不能再次将boolToString 应用到它的结果,因为它不接受 String 作为输入。

    所有这些函数的类型有哪些共同点,我们可以applyTwice

    简单:输入输出具有相同类型

    此类功能的示例:

    increment :: Int -> Int
    increment x = x + 1
    
    appendDot :: String -> String
    appendDot str = str ++ "."
    

    顺便说一句:函数类型签名用箭头 -&gt; 编写,就像 Haskell 中的 input -&gt; output

    此外,我们不仅可以在类型签名中使用StringInt具体类型,还可以使用类型变量

    类型变量总是小写字母开头,具体类型以大写字母开头。

    单个类型变量代表任意类型,并且在使用它的范围内,它只能代表那个,选定的类型,不管是哪一个。

    现在具体函数类型String -&gt; StringInt -&gt; Int 匹配通用类型:

     a -> a
    

    这匹配与输出具有相同输入的所有函数

    现在我们可以更接近applyTwice函数的类型了:

     applyTwice :: (a -> a) -> a -> a 
     applyTwice    f           x  = f (f a)
    

    括号(a -&gt; a)中的第一部分实际上是函数f的类型,它被应用了两次。

    我希望您了解这个applyTwice 函数的抽象

    它可以与 所有 具有 类型匹配 a -&gt; a 的函数一起使用,即 输入和输出类型相同的函数

    所以我们可以将incrementaddDot 提供给applyTwice

    例如,让我们创建一个新函数incTwice,它将某个数字增加两次。

    当然,我们希望使用我们的新工具 applyTwice 来构建在 increment 的基础上 - 一般来说,Haskell 的一个优点是我们可以非常优雅地重新组合和重用小函数,就像它们一样乐高积木:

     incTwice :: Int -> Int
     incTwice = applyTwice increment
    

    一些有趣的方面:​​

    1. 注意,incTwice的类型签名:Int -&gt; Int

    applyTwice 应用于 increment 时,Haskell 将类型 aInt 匹配。

    所以在这种情况下,incTwice Haskell 将 applyTwice 视为具有具体类型 (Int -&gt; Int) -&gt; Int -&gt; Int

    1. 还要注意incTwice 的类型是Int -&gt; Int,这意味着我们也可以将applyTwice 应用于它,以返回一个将数字加四的函数,并且该函数还将具有Int -&gt; Int 类型,我们可以无限频繁地使用applyTwice

    这不仅适用于这个例子,注意 Haskell 中每个可以applyTwice-ed 的函数通常可以无限频繁地应用!

    让我们看看一些 Haskell 语法!

    我将尝试简要介绍一些 Haskell 语法。

    Haskell 中的括号定义了子表达式到表达式的组合,或者 - 另一种看待它的方式 - 将表达式分解为子表达式。

    例子:

        ((1 - 1) * 10)      
        >>> 0
    

    是由应用于子表达式(1 - 1) 的子表达式* 和子表达式10 组成的表达式,而(1 - 1) 也是由应用于子表达式- 组成的子表达式11

    这可以写成一棵树:

    ((1 - 1) * 10):

             (*)
            /   \
           /     \
         (-)     10
        /   \
       /     \
      1      1
    

    方式完全不同

        (1 - (1 * 10))
        >>> -9
    

    其中有树:

             (-)
            /   \
           /     \
          1      (*)
                /   \
               /     \
              1      10
    

    括号也用于类型定义,描述函数类型。 如果没有给出括号,a -&gt; b -&gt; c -&gt; d 类型将隐式对应于a -&gt; (b -&gt; (c -&gt; d)),即-&gt;右结合, 这就是我们必须使用额外的括号来指示函数参数的原因!

    示例函数类型:

    • (a -&gt; b) -&gt; c -&gt; d 是函数的类型,它采用以下函数 键入a -&gt; b 作为参数并返回c -&gt; d 类型的新函数

    • (a -&gt; b) -&gt; (c -&gt; d)(a -&gt; b) -&gt; c -&gt; d 相同;)

    • (a -&gt; b -&gt; c) -&gt; d 是函数的类型,它以a -&gt; b -&gt; c 类型的函数作为参数并返回d 类型的值

    • a -&gt; (b -&gt; c) -&gt; d 是一个函数的类型,它接受一个a 类型的值,然后一个b -&gt; c 类型的函数作为参数并返回一个d 类型的值

      李>
    • a -&gt; (b -&gt; (c -&gt; d)) 是一个函数的类型,它接受 a 类型的值,然后返回一个新函数,该函数接受一个 b 类型的值并返回一个新函数,该函数的值是输入c 并返回d 类型的值。这和a -&gt; b -&gt; c -&gt; d一样!

    如您所见,始终可以添加显式/额外的括号,只要它们不偏离隐式分组,它们始终保留表达式/类型的含义, 因此,例如,您可以在 1 + 1 周围添加额外的括号,例如((((1 + 1))))((((1)) + ((((1)))))),不改变意思。

    总的来说,Haskell2010 报告states

    翻译:(e) 等价于e

    将表达式隐式分组为子表达式的一个方面称为关联性

    表达式:

        1 - 1 - 1 - 1
    

    可能理论上意味着两个不同的东西,这取决于子表达式的隐含分组方式。

    如果隐式分组就像这样设置括号:

    1. ((1 - 1) - 1) - 1,表达式的值为-2

    另一方面,如果隐含的分组是这样设置的括号:

    1. 1 - (1 - (1 - 1)),表达式有值0

    Haskell 标准(称为 Haskell2010 报告)要求运算符 - 更喜欢与左边的数字分组,因此 1 - 1 - 1 - 1 被解释为((1 - 1) - 1) - 1.

    确切地说,但使用 Haskell 报告的术语:

    运算符-左关联

    为了娱乐,请查看 Haskell2010 报告中的 corresponding table

    Haskell2010 报告还包含function application 规则的定义:

    函数应用程序编写为e1 e2。应用程序关联到左侧, 所以(f x) y中的括号可以省略。

    我们需要了解(f f) xf (f x)

    现在我们在 Haskell2010 报告中查找了 函数应用程序左关联,我们知道 f f x 被解释为 (f f) x

    我们还了解到,(e) 被翻译成e

    经过深思熟虑,可能会看到,由此得出(f f) x的意思是:

    **first** apply `f` to `f` and then apply _that_ to `x`. 
    

    现在,我们准备好了……

    通过查看子表达式比较 (f f) xf (f x)

    让我们尝试完全理解(f f) x 的含义,以及与f (f x) 的区别是什么,以及它们不是同一个表达式

    在描述表达式(f f) xf (f x)之间的区别的方法是太看这些表达式的子表达式:

    f (f x) 可以更明确地写成这样:

     let 
         intermediateResult = f x
         finalResult        = f intermediateResult
    
     in finalResult
    

    使用如下所示的表达式树:

                finalResult
                      =
                    apply
                  /       \
                 /         \
                /           \
               f       intermediateResult      
                              =
                            apply
                          /       \
                         /         \
                        f           x
    

    (f f) x 表示:

     let 
         intermediateFunction = f f
         finalResult          = intermediateFunction x
    
     in finalResult
    

    表达式树将是:

                finalResult
                      =
                    apply
                  /       \
                 /         \
                /           \
     intermediateFunction    x
              =
            apply
          /       \
         /         \
        f           f
    

    很明显,从表达式树中可以看出,这些是不同的表达式。 或者,有人可能会争辩说我们找不到任何句法转换(例如在 Haskell 报告中),在这些表达式之间来回转换,而不改变它们的含义。

    查看类型

    我还想展示另一种推理f f xf (f x) 之间差异的方法。

    此方法包括通过展示 f f x 如何与applyTwice 的类型签名。

    一开始可能很难意识到,但一个重要的观察是applyTwice 的类型签名,即(a -&gt; a) -&gt; a -&gt; aapplyTwice 可能拥有的唯一合理的类型签名 .如果它有任何不同,它不可能是我们心爱的applyTwice 函数的有效签名,至少不是我们所知道的,而且它采用的参数的顺序和数量 - 显然(我希望)。

    所以,如果我们简单地表明,f f xf (f x) 具有只是一个不同的类型,我们知道f f x 一定是一个不正确 实现applyTwice.

    总的来说:Haskell 中的类型检查器是检查程序是否损坏的强大工具,并且 有时查看函数的类型就足以了解它的作用。

    好的,继续旅程:

    如果我们使用f f x 作为函数体,而不是f (f x),让我们试试applyTwice 的类型签名会发生什么。

    野兽不仅会有不同的类型 - 这已经足以表明它是错误的 因为applyTwice 只有一个正确的类型签名(我挑战你,来加上另一个没有错的类型签名!) - 它也将有一个不可能构造的无限类型!

    好的,记住applyTwicef的类型签名:(a -&gt; a), 同样,这意味着f 的输入类型与输出类型相同。

    另外输入值xapplyTwice的第二个参数)也有那个(输入/输出-)类型。

    现在问题来了:如果我们真的使用f f x,那么f的类型必须满足这些要求:

    1. f 必须匹配a -&gt; a 的类型签名中指定的函数类型applyTwice

    2. f f 中的f 应用于自身,这意味着a -&gt; a 中的a 必须等于a -&gt; a,这是不可能的!

    好吧,可能这个不是很清楚,我们换个角度来看。

    1. 记住x通过applyTwice的类型签名绑定到类型a,这是f的输入(和输出)类型,

    2. 类型签名表明f 的类型为a -&gt; a

    3. 1234563必须输入a
    4. 但由于在f f f 应用于函数a -&gt; af 需要具有(a -&gt; a) -&gt; (a -&gt; a) 类型,并且

    5. 由于 to 应用于自身,因此它需要具有类型 ((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a))

    6. 由于 to 应用于自身,因此它需要具有类型 (((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a))) -&gt; (((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a)))

    7. ...以此类推,直到时间结束。

    这就是编译器抱怨无法构造无限类型的原因。

       badApplyTwice :: ???? 
       badApplyTwice f x = (f f) x -- error cannot construct infinite type!
    

    外面applyTwice(f f) x没有错,只是无聊……

    编译器创建无限类型的问题源于编译器如何尝试绑定 a 到单个类型,该类型将在整个表达式中使用,并使每个子表达式都开心.

    但是(f f)applyTwice 之外也有错误吗?

    不!

    让我们使用 GHCi 并定义一个 f 类型为 a -&gt; b

     λ> :{
     λ| let f :: a -> b
     λ|     f = undefined
     λ| :}         
    

    请注意,GHCi 中的多行表达式用 :{ ... :} 括起来。

    ...现在可以使用:t ghci 显示任何表达式的类型:

     λ> :t f
     f :: a -> b
    

    ...f f 的类型是b

     λ> :t (f f)
     (f f) :: b
    

    所以,如果我们将f 中的applyTwice 定义为a -&gt; a,那么我们会看到f f 的类型为a -&gt; a

     λ> :{
     λ| let f :: a -> a
     λ|     f = undefined
     λ| :}         
     λ> :t (f f)
     (f f) :: a -> a
    

    为什么? f xf :: a -&gt; a 的类型总是 a,因为我们总是取回我们放入 f 的类型(输入和输出类型相同),并且因为 f f 实际上就像 f x xf,我们得到f 的类型,即a -&gt; a

    那么,(f f) x 的类型是什么?

    嗯,它是x 的任何具体类型。让我们再次使用 ghci,并看一些例子:

    λ> :t (f f) 3
    (f f) 3 :: Num a => a
    
    λ> :t (f f) True
    (f f) True :: Bool
    
    λ> :t (f f) "test"
    (f f) "test" :: Data.String.IsString a => a
    
    λ> :t (f f) Nothing
    (f f) Nothing :: Maybe a
    
    λ> :t (f f) 12.3
    (f f) 12.3 :: Fractional a => a
    
    λ> :t (f f) 10000
    (f f) 10000 :: Num a => a
    
    λ> :t (f f) f
    (f f) f :: a -> a
    
    λ> :t f f f f f f f f f f f f
    f f f f f f f f f f f f :: a -> a
    

    好的,现在您看到(f f) x 通常没有无限类型, 让我们再举一个例子:

    λ> let g x = f f x
    λ> :t g
    g :: a -> a
    

    现在,f 很无聊,因为它只有两个实现:

    f x = undefined
    

    -或-

    f x = x
    

    类型为(a -&gt; a) 的函数的唯一终止实现是后者:

    f x = x
    

    也就是说,一个函数只返回原样,甚至不看它,更不用说对它做任何事情......

    为什么?很难理解,但试着想想a -&gt; a 的承诺。

    它保证您现在可以真正将任何东西放入其中,无论是什么类型,并且保证您得到相同类型的值。

    所以所有f 可以对其参数执行计算结果,这是它必须能够对每个类型执行的操作!

    例如如果我们说f 应该增加输入,那么我们不能将f 应用于例如一个Bool,如果我们想让f 使大写 成为输入,我们不能将它应用于不是String 的东西,等等。

    但是我们可以将带有签名a -&gt; a 的函数应用于any类型正是该类型签名所承诺的。

    所以,f 基本上很无聊,但是当我们在玩 ghci 时,编译器并没有抱怨无限类型

    问:现在,为什么它在一个函数中会出现问题,例如applyTwiceWrong(见下文)?

    A:因为编译器想要绑定 a一个 类型,这满足类型签名中的所有出现,以及 (sub -) 函数体中的表达式。

    让我们仔细看看错误:

    λ> :{
    λ| let applyTwiceWrong :: (a -> a) -> a -> a
    λ|     applyTwiceWrong f x = f f x
    λ| :}
    
    <interactive>:229:24: error:
        • Couldn't match expected type ‘a’ with actual type ‘a -> a’
          ‘a’ is a rigid type variable bound by
            the type signature for:
              applyTwice :: forall a. (a -> a) -> a -> a
            at <interactive>:228:19
        • In the first argument of ‘f’, namely ‘f’
          In the expression: f f x
          In an equation for ‘applyTwiceWrong’: applyTwiceWrong f x = f f x
        • Relevant bindings include
            x :: a (bound at <interactive>:229:18)
            f :: a -> a (bound at <interactive>:229:16)
            applyTwiceWrong :: (a -> a) -> a -> a (bound at <interactive>:229:5)
    

    第一个要点明确指出:

    (...) a 是一个刚性类型变量受类型签名约束:applyTwiceWrong (...)

    applyTwiceWrong 编写类型签名的唯一可能的方法涉及到诡异的东西,即higher-ranked-types

    只有在激活编译器扩展后才能使用排名较高的类型:

    λ> :set -XRankNTypes 
    

    我们可以定义applyTwiceWrong 的变体,我们称之为applyTwiceBoring,它具有相同的body,但类型不同:

    λ> :{
    λ| applyTwiceBoring :: (forall b . b -> b) -> a -> a
    λ| applyTwiceBoring f x = (f f) x
    λ| :}
    λ> 
    

    我们现在有了用于xaapplyTwiceBoring 的返回类型,我们引入了一个新变量bforall b . b -&gt; b

    b 现在可能在applyTwiceBoring 的子表达式之间变化,因为在applyTwiceBoring 中,b 是免费的

    记住(f f) x 的子表达式是:

                finalResult
                      =
                    apply
                  /       \
                 /         \
                /           \
     intermediateFunction    x
              =
            apply
          /       \
         /         \
        f           f
    

    intermediateFunctionb 将是b -&gt; b,在intermediateFunction xb 将具有a -&gt; a 类型。

    现在我们可以传递给applyTwiceBoring唯一终止函数是f x = x - 礼貌地说 - 限制

    救援组合!

    现在,一切都失去了吗?

    不!你对(f f) x 背后的直觉非常好!

    这背后的想法是:让我们撰写 f自身,然后将那个应用到x

    嗯,在 Math 和 Haskell 中 composition 两个函数是这样完成的: F 。 g

    这意味着:将f 应用于g 的结果

    在 Haskell 中有操作符.,简单定义为:

     (f . g) x = f (g x)
    

    那个我们可以用来拯救(f f) x,然后变成:

         (f . f) x
    

    所以完整的功能是:

        applyTwice f x = (f . f) x
    

    我们使用等式推理证明这与f (f x)相同:

        applyTwice f x = (f . f) x       [insert the definition of . ]
        applyTwice f x = f (f x)         QED
    

    既然 Haskell 真的很棒,我们可以写

        applyTwice f x = (f . f) x
    

    作为

        applyTwice f = f . f 
    

    我希望这会有所帮助,祝你旅途愉快;)

    【讨论】:

    • 为什么(a -&gt; a) -&gt; (a -&gt; a)申请f 后仍然继续((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a))
    • @zero_coding 好点,我错过了一个重要方面。答案是因为在函数applyTwice 中,f 固定为a -&gt; aax 的类型,因此类型推断最终构建了一个无限类型。我会改进我的答案,请提供反馈。
    • 我真正不明白的是,f f 只会在applyTwice 函数中应用它们自己两次,应用后对我来说(a -&gt; a) -&gt; (a -&gt; a) 清楚。 ((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a)) 怎么会因为 x = a
    • @zero_coding 差不多,就像这样:如果f :: a -&gt; a 然后表达式f f 强制类型推断匹配aa -&gt; a,因为 f in f f 当然也有 a -&gt; a 类型,现在将每个 a 替换为 a-&gt;a 直到无穷大,你会得到 a -&gt; a 也必须匹配 (a -&gt; a) -&gt; (a -&gt; a)((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a))(((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a)) -&gt; ((a -&gt; a) -&gt; (a -&gt; a))) ...等
    猜你喜欢
    • 2015-10-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多