Haskell 函数组合及其表示

Haskell 函数组合及其表示,haskell,pointfree,Haskell,Pointfree,我想知道: 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 前四个功能是相同的 您正试图将double应用于inc。这将不起作用,因为i

我想知道:

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

前四个功能是相同的


您正试图将
double
应用于
inc
。这将不起作用,因为
inc
不能相乘

double $ inc
-- is the same as
double inc
如果添加类型规格,您将看到:

inc :: Integer -> Integer
double :: Integer -> Integer
double
接受一个
整数
,但您试图传递一个
整数->整数

请注意,在Haskell中明确说明顶级函数的类型是一种很好的做法,因为这些类型通常会告诉我们很多关于函数和程序的信息。

1)是的。这些功能完全相同

2) 要了解
func5
不起作用的原因,只需扩展其定义:

func5

-- Definition of `func5`
= double $ inc

-- Definition of `($)`
= double inc

-- Definition of `double`
= 2 * inc

-- Definition of `inc`
= 2 * (1 +)
编译器抱怨是因为
(1+)
是一个函数,不能将函数加倍。

$
(称为应用运算符)不是另一种编写
(函数组合运算符)的方式。您可以通过使用ghci看到它们不同:

你可以用括号代替
$
。因此
func2
func5
可以重写为:

func2 x = double (inc x)
func5 = double (inc)
但是,
double
需要一个类型为
Num a=>a
的值,而您正在传递一个类型为
Num a=>a->a
的值,这就是它不起作用的原因

您可以阅读有关
$
的更多信息

这些功能完全相同吗

事实上,没有!有一些非常微妙的区别。首先,请阅读有关的文章。简而言之,默认情况下,如果类多态函数是“显然”函数还是非“显然”函数,则它们会被赋予不同的类型。在您的代码中,这种差异不会表现出来,因为
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
func4
——它们不是“明显”的函数——被赋予了单态类型:

*Main> :t func1
func1 :: Integer -> Integer
*Main> :t func4
func4 :: Integer -> Integer
*Main> :t func2
func2 :: Num a => a -> a
*Main> :t func3
func3 :: Num a => a -> a
func2
func3
被赋予多态类型:

*Main> :t func1
func1 :: Integer -> Integer
*Main> :t func4
func4 :: Integer -> Integer
*Main> :t func2
func2 :: Num a => a -> a
*Main> :t func3
func3 :: Num a => a -> a
第二个细微的区别是,这些实现可能具有(非常细微的)不同的评估行为。由于
()
($)
是函数,您可能会发现调用
func1
func2
需要进行一点计算,然后才能运行。例如,
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 = double $ inc        -- doesn't work
因为你不想编译它!想象一下。让我们看看如何评估
func5 3
。我们会看到我们“陷入困境”

现在我们试着把一个函数乘以二。目前,我们还没有说函数的乘法应该是什么(或者,在本例中,甚至没有说“2”应该是什么!),所以我们“被卡住了”——我们无法进一步计算。那不好!我们不想“被困”在如此复杂的术语中——我们只想被困在简单的术语中,比如实际的数字、函数等等


我们本可以从一开始就观察到,
double
只知道如何处理可以成倍增长的事物,而
inc
不是可以成倍增长的事物,从而避免这一切。这就是类型系统所做的:它进行这样的观察,当很明显有什么古怪的事情要发生时,它拒绝编译。

点运算符
()
用于将函数“管道”在一起,这样表达式
f。g
是一个函数,其中
g
的结果用作
f
的参数。美元运算符
($)
用于为函数提供参数,因此表达式
f$x
是一个值,其中
x
作为
f
的参数提供。对于OP:有一个非常自然的概念,将函数加倍为交换环(数字应该是交换环)。使用Haskell破碎的数字类型类是不可能的。您可以通过定义
instance Num b=>Num(a->b)来解决这个问题,其中f+g=\a->fa+ga;f*g=\a->f*g;fromInteger=const。fromInteger;--
然后double$inc将实现您所期望的功能。@Fixnum实际上,您可以将任何函数加倍到一个阿贝尔群中:
(2*f)(x)=f(x)+f(x)
@Joker\u vD-实际上,您可以将任何magma值函数加倍(但会损失一些好的属性)。我很抱歉没有完全概括我的答案:)在模块结构中,你可以乘以环元素(如果你破解了更多的实例),而不仅仅是整数,这可能是OP所期望的。当然,你完全正确。回答得太好了!(精确精确的还原顺序,出色的类型系统介绍等)