我想知道:
1) 以下功能是否完全相同:
inc = (+1)
double = (*2)
func1 = double . inc
func2 x = double $ inc x
func3 x = double (inc x)
func4 = \x -> double (inc x)
2)为什么不func5
编译?
func5 = double $ inc -- doesn't work
这些功能是否完全相同?
其实,不!有一些非常细微的区别。首先,阅读可怕的单态限制。简而言之,类多态函数默认情况下会被赋予不同的类型,无论它们是否是“明显”的函数。在您的代码中,这种差异不会体现出来,因为inc
并且double
不是“明显”的函数,因此被赋予了单态类型。但如果我们稍作改动:
inc, double :: Num a => a -> a
inc = (+1)
double = (*2)
func1 = double . inc
func2 x = double $ inc x
func3 x = double (inc x)
func4 = \x -> double (inc x)
然后在 ghci 中我们可以观察到func1
and func4
——它们不是“明显”的函数——被赋予了一个单态类型:
*Main> :t func1
func1 :: Integer -> Integer
*Main> :t func4
func4 :: Integer -> Integer
而func2
andfunc3
被赋予一个多态类型:
*Main> :t func2
func2 :: Num a => a -> a
*Main> :t func3
func3 :: Num a => a -> a
第二个细微差别是这些实现可能具有(非常轻微)不同的评估行为。由于(.)
and($)
是函数,您可能会发现调用func1
andfunc2
需要一些评估才能运行。例如,也许第一次调用func1 3
收益是这样的:
func1 3
= {- definition of func1 -}
(double . inc) 3
= {- definition of (.) -}
(\f g x -> f (g x)) double inc 3
= {- beta reduction -}
(\g x -> double (g x)) inc 3
= {- beta reduction -}
(\x -> double (inc x)) 3
而第一次调用,例如,func4 3
以更直接的方式到达这一点:
func3 3
= {- definition of func3 -}
(\x -> double (inc x)) 3
不过,我不会太担心这个。我希望在启用优化的 GHC 中,对两者的调用饱和(.)
并($)
内联,从而消除这种可能的差异;即使没有,这确实是一个非常小的成本,因为这可能只会在每个定义中发生一次(而不是每次调用一次)。
为什么不
func5
编译?
因为你不希望它编译!想象一下它做到了。让我们看看我们将如何评估func5 3
. 我们会看到我们“陷入困境”。
func5 3
= {- definition of func5 -}
(double $ inc) 3
= {- definition of ($) -}
(\f x -> f x) double inc 3
= {- beta reduction -}
(\x -> double x) inc 3
= {- beta reduction -}
double inc 3
= {- definition of double -}
(\x -> x*2) inc 3
= {- beta reduction -}
(inc * 2) 3
= {- definition of inc -}
((\x -> x+1) * 2) 3
现在我们试图将一个函数乘以 2。目前,我们还没有说函数的乘法应该是什么(或者甚至,在这种情况下,“二”应该是什么!),所以我们“卡住了”——我们无法进一步评估。这不好!我们不想“卡在”这样一个复杂的术语上——我们只想卡在简单的术语上,比如实际数字、函数之类的东西。
我们可以通过一开始就观察到double
只知道如何操作可以乘法inc
的东西而不是可以乘法的东西来防止这整个混乱。所以这就是类型系统所做的:它进行这样的观察,并且当很明显会发生一些古怪的事情时拒绝编译。
1) 是的。这些功能都完全相同
2)要查看为什么func5
不起作用,只需扩展其定义:
func5
-- Definition of `func5`
= double $ inc
-- Definition of `($)`
= double inc
-- Definition of `double`
= 2 * inc
-- Definition of `inc`
= 2 * (1 +)
编译器抱怨,因为(1 +)
它是一个函数,你不能加倍函数。
前四个功能是相同的。
您正在尝试double
申请inc
. 这是行不通的,因为inc
不能成倍增加。
double $ inc
-- is the same as
double inc
如果您添加类型规范,您会看到它:
inc :: Integer -> Integer
double :: Integer -> Integer
double
需要一个,Integer
但你正试图通过它一个Integer -> Integer
.
请注意,在 Haskell 中明确声明顶级函数的类型是一种很好的做法,因为这些通常会说明很多关于函数和程序的信息。
$
(称为应用运算符)不是一种不同的编写方式.
(函数组合运算符)。您可以通过使用ghci看到它们不一样:
>:t ($)
($) :: (a -> b) -> a -> b
:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c
你总是可以$
用括号代替。所以func2
和func5
可以改写为:
func2 x = double (inc x)
func5 = double (inc)
但是double
需要一个类型的值,Num a => a
并且您正在向它传递一个类型的值,Num a => a -> a
这就是它不起作用的原因。