【问题标题】:Function composition and its representations函数组合及其表示
【发布时间】:2013-10-27 17:34:47
【问题描述】:

我想知道:

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

【问题讨论】:

  • 点运算符(.)用于将函数“管道”在一起,因此表达式f . g是一个函数,其中使用g的结果作为f 的参数。美元运算符($) 用于为函数提供参数,因此表达式f $ x 是一个,其中x 作为f 的参数提供。

标签: haskell pointfree


【解决方案1】:

这些功能完全一样吗?

其实,不!有一些非常细微的区别。首先,阅读dreaded monomorphism restriction。简而言之,类多态函数默认情况下会被赋予不同的类型,无论它们是否是“明显”的函数。在您的代码中,这种差异不会体现出来,因为 incdouble 不是“明显”的函数,因此被赋予了单态类型。但如果我们稍作改动:

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 中我们可以观察到 func1func4 ——它们不是“明显”的函数——被赋予了一个单态类型:

*Main> :t func1
func1 :: Integer -> Integer
*Main> :t func4
func4 :: Integer -> Integer

func2func3 被赋予了多态类型:

*Main> :t func2
func2 :: Num a => a -> a
*Main> :t func3
func3 :: Num a => a -> a

第二个细微差别是这些实现可能具有(非常轻微)不同的评估行为。由于(.)($) 是函数,您可能会发现调用func1func2 需要进行一些评估才能运行。例如,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 不是可以乘法的东西来防止这整个混乱。所以这就是类型系统所做的:它会进行这样的观察,并在很明显接下来会发生一些古怪的事情时拒绝编译。

【讨论】:

  • 绝妙的答案! (减少序列的精确度,类型系统的大量引入等)
【解决方案2】:

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

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

func5

-- Definition of `func5`
= double $ inc

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

-- Definition of `double`
= 2 * inc

-- Definition of `inc`
= 2 * (1 +)

编译器抱怨是因为(1 +) 是一个函数,你不能加倍函数。

【讨论】:

  • 致 OP:有一个非常自然的概念,即把一个函数加倍成一个交换环(数字应该是)。使用 Haskell 损坏的数字类型类是不可能的。您可以通过定义 instance Num b => Num (a -> b) where f + g = \a -> f a + g a; f * g = \a -> f a * g a; fromInteger = const . fromInteger; -- ... 来解决这个问题,然后 double $ inc 会按照您的预期进行。
  • @Fixnum 实际上,您可以将任何函数加倍成一个阿贝尔群:(2 * f)(x) = f(x) + f(x)
  • @Joker_vD - 实际上,您可以将任何岩浆值函数加倍(损失一些好的属性)。我很抱歉没有完全概括我的答案:) 使用模块结构,您可以乘以环元素(如果您破解更多实例),而不仅仅是整数,这可能是 OP 所期望的。当然,你是完全正确的。
【解决方案3】:

前四个函数是相同的。


您正在尝试将double 应用于inc。这行不通,因为inc 不能相乘。

double $ inc
-- is the same as
double inc

如果您添加类型规范,您会看到它:

inc :: Integer -> Integer
double :: Integer -> Integer

double 接受 Integer,但您试图将 Integer -> Integer 传递给它。

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

【讨论】:

    【解决方案4】:

    $(称为应用运算符)与.(函数组合运算符)的写法不同。使用 ghci 可以看到它们不一样:

    >:t ($)
    ($) :: (a -> b) -> a -> b
    
    :t (.)
    (.) :: (b -> c) -> (a -> b) -> a -> c
    

    您始终可以用括号替换$。所以func2func5可以改写为:

    func2 x = double (inc x)
    func5 = double (inc)
    

    但是double 需要一个Num a => a 类型的值,而你传递给它的是一个Num a => a -> a 类型的值,这就是它不起作用的原因。

    你可以阅读更多关于$here.here的信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-04-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-13
      • 2016-08-25
      • 2021-03-22
      • 1970-01-01
      相关资源
      最近更新 更多