为什么 (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 ++ "."
顺便说一句:函数类型签名用箭头 -> 编写,就像 Haskell 中的 input -> output。
此外,我们不仅可以在类型签名中使用String 或Int 等具体类型,还可以使用类型变量。
类型变量总是以小写字母开头,具体类型以大写字母开头。
单个类型变量代表任意类型,并且在使用它的范围内,它只能代表那个,选定的类型,不管是哪一个。
现在具体函数类型String -> String 或Int -> Int 匹配通用类型:
a -> a
这匹配与输出具有相同输入的所有函数。
现在我们可以更接近applyTwice函数的类型了:
applyTwice :: (a -> a) -> a -> a
applyTwice f x = f (f a)
括号(a -> a)中的第一部分实际上是函数f的类型,它被应用了两次。
我希望您了解这个applyTwice 函数的抽象:
它可以与 所有 具有 类型匹配 a -> a 的函数一起使用,即 输入和输出类型相同的函数
所以我们可以将increment 和addDot 提供给applyTwice。
例如,让我们创建一个新函数incTwice,它将某个数字增加两次。
当然,我们希望使用我们的新工具 applyTwice 来构建在 increment 的基础上 - 一般来说,Haskell 的一个优点是我们可以非常优雅地重新组合和重用小函数,就像它们一样乐高积木:
incTwice :: Int -> Int
incTwice = applyTwice increment
一些有趣的方面:
- 注意,
incTwice的类型签名:Int -> Int。
当 applyTwice 应用于 increment 时,Haskell 将类型 a 与 Int 匹配。
所以在这种情况下,incTwice Haskell 将 applyTwice 视为具有具体类型 (Int -> Int) -> Int -> Int。
- 还要注意
incTwice 的类型是Int -> Int,这意味着我们也可以将applyTwice 应用于它,以返回一个将数字加四的函数,并且该函数还将具有Int -> Int 类型,我们可以无限频繁地使用applyTwice。
这不仅适用于这个例子,注意 Haskell 中每个可以applyTwice-ed 的函数通常可以无限频繁地应用!
让我们看看一些 Haskell 语法!
我将尝试简要介绍一些 Haskell 语法。
Haskell 中的括号定义了子表达式到表达式的组合,或者 - 另一种看待它的方式 - 将表达式分解为子表达式。
例子:
((1 - 1) * 10)
>>> 0
是由应用于子表达式(1 - 1) 的子表达式* 和子表达式10 组成的表达式,而(1 - 1) 也是由应用于子表达式- 组成的子表达式1 和1。
这可以写成一棵树:
((1 - 1) * 10):
(*)
/ \
/ \
(-) 10
/ \
/ \
1 1
方式完全不同
(1 - (1 * 10))
>>> -9
其中有树:
(-)
/ \
/ \
1 (*)
/ \
/ \
1 10
括号也用于类型定义,描述函数类型。
如果没有给出括号,a -> b -> c -> d 类型将隐式对应于a -> (b -> (c -> d)),即-> 是右结合,
这就是我们必须使用额外的括号来指示函数参数的原因!
示例函数类型:
(a -> b) -> c -> d 是函数的类型,它采用以下函数
键入a -> b 作为参数并返回c -> d 类型的新函数
(a -> b) -> (c -> d) 与(a -> b) -> c -> d 相同;)
(a -> b -> c) -> d 是函数的类型,它以a -> b -> c 类型的函数作为参数并返回d 类型的值
-
a -> (b -> c) -> d 是一个函数的类型,它接受一个a 类型的值,然后一个b -> c 类型的函数作为参数并返回一个d 类型的值
李>
a -> (b -> (c -> d)) 是一个函数的类型,它接受 a 类型的值,然后返回一个新函数,该函数接受一个 b 类型的值并返回一个新函数,该函数的值是输入c 并返回d 类型的值。这和a -> b -> c -> d一样!
如您所见,始终可以添加显式/额外的括号,只要它们不偏离隐式分组,它们始终保留表达式/类型的含义,
因此,例如,您可以在 1 + 1 周围添加额外的括号,例如((((1 + 1)))) 或
((((1)) + ((((1)))))),不改变意思。
总的来说,Haskell2010 报告states:
翻译:(e) 等价于e。
将表达式隐式分组为子表达式的一个方面称为关联性。
表达式:
1 - 1 - 1 - 1
可能理论上意味着两个不同的东西,这取决于子表达式的隐含分组方式。
如果隐式分组就像这样设置括号:
-
((1 - 1) - 1) - 1,表达式的值为-2,
另一方面,如果隐含的分组是这样设置的括号:
-
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) x 与f (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) x 和 f (f x)
让我们尝试完全理解(f f) x 的含义,以及与f (f x) 的区别是什么,以及它们不是同一个表达式。
在描述表达式(f f) x和f (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 x 和f (f x) 之间差异的方法。
此方法包括通过展示 f f x 如何与applyTwice 的类型签名。
一开始可能很难意识到,但一个重要的观察是applyTwice 的类型签名,即(a -> a) -> a -> a 是applyTwice 可能拥有的唯一合理的类型签名 .如果它有任何不同,它不可能是我们心爱的applyTwice 函数的有效签名,至少不是我们所知道的,而且它采用的参数的顺序和数量 - 显然(我希望)。
所以,如果我们简单地表明,f f x 与f (f x) 具有只是一个不同的类型,我们知道f f x 一定是一个不正确 实现applyTwice.
总的来说:Haskell 中的类型检查器是检查程序是否损坏的强大工具,并且
有时查看函数的类型就足以了解它的作用。
好的,继续旅程:
如果我们使用f f x 作为函数体,而不是f (f x),让我们试试applyTwice 的类型签名会发生什么。
野兽不仅会有不同的类型 - 这已经足以表明它是错误的 因为applyTwice 只有一个正确的类型签名(我挑战你,来加上另一个没有错的类型签名!) - 它也将有一个不可能构造的无限类型!
好的,记住applyTwice中f的类型签名:(a -> a),
同样,这意味着f 的输入类型与输出类型相同。
另外输入值x(applyTwice的第二个参数)也有那个(输入/输出-)类型。
现在问题来了:如果我们真的使用f f x,那么f的类型必须满足这些要求:
f 必须匹配a -> a 的类型签名中指定的函数类型applyTwice
f f 中的f 应用于自身,这意味着a -> a 中的a 必须等于a -> a,这是不可能的!
好吧,可能这个不是很清楚,我们换个角度来看。
记住x通过applyTwice的类型签名绑定到类型a,这是f的输入(和输出)类型,
类型签名表明f 的类型为a -> a,
1234563必须输入a
但由于在f f f 应用于函数a -> a,f 需要具有(a -> a) -> (a -> a) 类型,并且
由于 to 应用于自身,因此它需要具有类型 ((a -> a) -> (a -> a)) -> ((a -> a) -> (a -> a))
由于 to 应用于自身,因此它需要具有类型 (((a -> a) -> (a -> a)) -> ((a -> a) -> (a -> a))) -> (((a -> a) -> (a -> a)) -> ((a -> a) -> (a -> a)))
...以此类推,直到时间结束。
这就是编译器抱怨无法构造无限类型的原因。
badApplyTwice :: ????
badApplyTwice f x = (f f) x -- error cannot construct infinite type!
外面applyTwice、(f f) x没有错,只是无聊……
编译器创建无限类型的问题源于编译器如何尝试绑定 a 到单个类型,该类型将在整个表达式中使用,并使每个子表达式都开心.
但是(f f) 在applyTwice 之外也有错误吗?
不!
让我们使用 GHCi 并定义一个 f 类型为 a -> 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 -> a,那么我们会看到f f 的类型为a -> a:
λ> :{
λ| let f :: a -> a
λ| f = undefined
λ| :}
λ> :t (f f)
(f f) :: a -> a
为什么? f x 和 f :: a -> a 的类型总是 a,因为我们总是取回我们放入 f 的类型(输入和输出类型相同),并且因为 f f 实际上就像 f x x 为f,我们得到f 的类型,即a -> 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 -> a) 的函数的唯一终止实现是后者:
f x = x
也就是说,一个函数只返回原样,甚至不看它,更不用说对它做任何事情......
为什么?很难理解,但试着想想a -> a 的承诺。
它保证您现在可以真正将任何东西放入其中,无论是什么类型,并且保证您得到相同类型的值。
所以所有f 可以对其参数执行计算结果,这是它必须能够对每个类型执行的操作!
例如如果我们说f 应该增加输入,那么我们不能将f 应用于例如一个Bool,如果我们想让f 使大写 成为输入,我们不能将它应用于不是String 的东西,等等。
但是我们可以将带有签名a -> 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
λ| :}
λ>
我们现在有了用于x 的a 和applyTwiceBoring 的返回类型,我们引入了一个新变量b 和forall b . b -> b。
b 现在可能在applyTwiceBoring 的子表达式之间变化,因为在applyTwiceBoring 中,b 是免费的!
记住(f f) x 的子表达式是:
finalResult
=
apply
/ \
/ \
/ \
intermediateFunction x
=
apply
/ \
/ \
f f
在intermediateFunction 中b 将是b -> b,在intermediateFunction x 中b 将具有a -> 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
我希望这会有所帮助,祝你旅途愉快;)