【问题标题】:How does currying work?柯里化是如何工作的?
【发布时间】:2011-10-02 21:26:41
【问题描述】:

总的来说,我对 Haskell 和 FP 很陌生。我已经阅读了许多描述什么是柯里化的著作,但我还没有找到关于它实际工作原理的解释。

这是一个函数:(+) :: a -> (a -> a) 如果我执行(+) 4 7,则该函数采用4 并返回一个采用7 并返回11 的函数。但是 4 会发生什么?第一个函数对4 有什么作用? (a -> a)7 有什么作用?

当我想到一个更复杂的函数时,事情变得更加混乱:

max' :: Int -> (Int -> Int)
max' m n | m > n = m
         | otherwise = n

(Int -> Int) 将其参数与什么进行比较?它只需要一个参数,但它需要两个来做m > n

【问题讨论】:

  • stackoverflow.com/questions/1352855/… -- 给你的好资源!
  • @progo:这是一个很好的链接,但我认为这不是提问者要问的。这显然是在询问柯里化是如何实现的,而不是它是什么。
  • 你只是构造了一个新的表达式,为什么你要定义新的东西。除非您尝试显示或强制它,否则不会计算任何表达式。我建议您阅读维基百科中有关 lambda 演算的术语

标签: haskell currying


【解决方案1】:

理解高阶函数

Haskell 作为一种函数式语言,支持高阶函数 (HOF)。在数学中,HOF 被称为functionals,但您不需要任何数学来理解它们。在通常的命令式编程中,比如在 Java 中,函数可以接受值,比如整数和字符串,对它们做一些事情,然后返回一个其他类型的值。

但是,如果函数本身与值没有什么不同,并且您可以接受一个函数作为参数或从另一个函数返回它呢? f a b c = a + b - c 是一个无聊的函数,它将 ab 相加,然后减去 c。但是这个函数可能会更有趣,如果我们可以概括它,如果我们有时想将ab 相加,但有时会相乘怎么办?或者除以c而不是减去?

请记住,(+) 只是一个返回一个数字的 2 个数字的函数,它没有什么特别之处,因此任何返回一个数字的 2 个数字的函数都可以代替它。写g a b c = a * b - ch a b c = a + b / c 等等对我们来说并不合适,我们需要一个通用的解决方案,毕竟我们是程序员!在 Haskell 中它是如何完成的:

let f g h a b c = a `g` b `h` c in f (*) (/) 2 3 4 -- returns 1.5

你也可以返回函数。下面我们创建一个函数,它接受一个函数和一个参数并返回另一个函数,该函数接受一个参数并返回一个结果。

let g f n = (\m -> m `f` n); f = g (+) 2 in f 10 -- returns 12

(\m -> m `f` n) 构造是 1 个参数 manonymous function,它将 f 应用于 mn。基本上,当我们调用g (+) 2 时,我们创建了一个有一个参数的函数,它只会在它收到的任何内容上加 2。所以let f = g (+) 2 in f 10 等于 12,let f = g (*) 5 in f 5 等于 25。

(另见my explanation of HOFs,以Scheme为例。)

理解柯里化

Currying 是一种将多个参数的函数转换为 1 个参数的函数的技术,该函数返回一个具有 1 个参数的函数,该函数返回一个具有 1 个参数的函数......直到它返回一个值。这比听起来容易,例如我们有一个有 2 个参数的函数,例如 (+)

现在想象一下,你可以只给它一个参数,它会返回一个函数?您可以稍后使用此函数将这个现在包含在这个新函数中的 1st 参数添加到其他内容中。例如:

f n = (\m -> n - m)
g = f 10
g 8 -- would return 2
g 4 -- would return 6

猜猜看,Haskell 默认对所有函数进行柯里化。从技术上讲,Haskell 中没有多参数的函数,只有一个参数的函数,其中一些可能返回一个参数的新函数。

从类型上可以看出。在解释器中写入:t (++),其中(++)是将2个字符串连接在一起的函数,它将返回(++) :: [a] -> [a] -> [a]。类型不是[a],[a] -> [a],而是[a] -> [a] -> [a],也就是说(++)接受一个列表,返回一个[a] -> [a]类型的函数。这个新函数可以接受另一个列表,它最终会返回一个 [a] 类型的新列表。

这就是为什么 Haskell 中的函数应用程序语法没有括号和逗号的原因,将 Haskell 的 f a b c 与 Python 或 Java 的 f(a, b, c) 进行比较。这不是什么奇怪的审美决定,在 Haskell 函数应用程序中从左到右,所以 f a b c 实际上是 (((f a) b) c),这完全有道理,一旦你知道 f 是默认柯里化的。

然而,在类型中,关联是从右到左的,所以[a] -> [a] -> [a] 等价于[a] -> ([a] -> [a])。它们在 Haskell 中是一样的,Haskell 对待它们完全相同。这是有道理的,因为当你只应用一个参数时,你会得到一个 [a] -> [a] 类型的函数。

另一方面,检查map:(a -> b) -> [a] -> [b]的类型,它接收一个函数作为它的第一个参数,这就是它有括号的原因。

要真正深入了解柯里化的概念,请尝试在解释器中找到以下表达式的类型:

(+)
(+) 2
(+) 2 3
map
map (\x -> head x)
map (\x -> head x) ["conscience", "do", "cost"]
map head
map head ["conscience", "do", "cost"]

部分应用和部分

现在您了解了 HOF 和柯里化,Haskell 为您提供了一些语法来缩短代码。当你调用一个带有 1 个或多个参数的函数来取回一个仍然接受参数的函数时,它被称为 partial application

您已经了解,您可以只部分应用函数,而不是创建匿名函数,因此您可以编写 (replicate 3),而不是编写 (\x -> replicate 3 x)。但是如果你想要一个除法 (/) 运算符而不是 replicate 怎么办?对于中缀函数,Haskell 允许您使用任一参数部分应用它。

这称为sections(2/) 等价于(\x -> 2 / x)(/2) 等价于(\x -> x / 2)。使用反引号,您可以获取任何二进制函数的一部分:(2`elem`) 等价于 (\xs -> 2 `elem` xs)

但是请记住,任何函数在 Haskell 中默认情况下都是柯里化的,因此总是接受一个参数,因此节实际上可以与任何函数一起使用:让 (+^) 是一个奇怪的函数,它对 4 个参数求和,然后 let (+^) a b c d = a + b + c in (2+^) 3 4 5 返回 14 .

作曲

编写简洁灵活的代码的其他便捷工具是compositionapplication operator。组合运算符(.) 将函数链接在一起。应用运算符($) 只是将左侧的函数应用于右侧的参数,因此f $ x 等价于f x。但是($) 在所有运算符中的优先级最低,所以我们可以用它来去掉括号:f (g x y) 等价于f $ g x y

当我们需要对同一个参数应用多个函数时,它也很有帮助:map ($2) [(2+), (10-), (20/)] 将产生 [4,8,10](f . g . h) (x + y + z)f (g (h (x + y + z)))f $ g $ h $ x + y + zf . g . h $ x + y + z 是等价的,但 (.)($) 是不同的东西,所以请阅读 Haskell: difference between . (dot) and $ (dollar sign)parts from Learn You a Haskell 以了解区别。

【讨论】:

  • 很好的解释。问题:在实现中,例如一个具有 3 个或更多参数的函数,其中我第一次传递 2,它是否“减少”调用每个参数一个参数的函数(在这种情况下仍会返回一个需要 1 个参数的函数)?另外,如果我调用具有完整参数计数的函数,它是否会执行任何“快捷方式”,还是会经历与我上面提到的相同的事情?
  • Haskell中3个参数的函数是a -> b -> c -> d类型的函数,相当于a -> b -> (c -> d),即等价于 a -> (b -> (c -> d))。所以是的,无论您的函数需要多少参数来完全评估,currying 都是完全相同的。 GHC 是否将中间代码中的柯里化函数重写为多变量函数——这个我不知道。那是你的问题吗?我可以更新我的答案以更清楚地解释这些想法。
  • 我的第一个问题:我推断 GHC 正在做某种归约,直到它耗尽给定的参数数量(每个归约返回一个 x->y 函数并将其应用于下一个,如果有的话,论据)。根据我的理解,第二个问题不如第一个重要。神秘的面纱已经揭开,谢谢您,先生。
【解决方案2】:

如果您来自类 C 语言,它们的语法可能会帮助您理解它。例如在 PHP 中 add 函数可以这样实现:

function add($a) {
    return function($b) use($a) {
         return $a + $b;
    };
}

【讨论】:

  • 唯一不同的是,这在 Haskell 中表现得更好。
  • 毫无疑问。我只是觉得显式柯里化(和显式闭包)可能有助于更好地理解这些概念
【解决方案3】:

如果 Haskell 没有内置对它的支持,那么考虑一下如何将 curry 实现为高阶函数可能会有所帮助。这是一个适用于两个参数的函数的 Haskell 实现。

curry :: (a -> b -> c) -> a -> (b -> c)
curry f a = \b -> f a b

现在您可以将curry 传递给两个参数和第一个参数的函数,它会在一个参数上返回一个函数(这是一个闭包示例。)

在 ghci 中:

Prelude> let curry f a = \b -> f a b
Prelude> let g = curry (+) 5
Prelude> g 10
15
Prelude> g 15
20
Prelude> 

幸运的是,我们不必在 Haskell 中执行此操作(如果您想要柯里化,您可以在 Lisp 中执行此操作),因为语言中内置了支持。

【讨论】:

  • 我想说,例如,将使用元组的非柯里化函数转换为不使用元组的柯里化函数更有意义。
  • 也许,我认为没有元组的示例与 Haskell 允许的示例非常相似,而中间数据结构可能会造成混淆。
【解决方案4】:

你可以把它想象成函数存储参数并返回一个只需要其他参数的新函数。新函数已经知道第一个参数,因为它与函数一起存储。这由编译器在内部处理。如果你想知道它到底是如何工作的,你可能会对this page 感兴趣,尽管如果你是 Haskell 的新手,它可能会有点复杂。

如果函数调用完全饱和(因此所有参数同时传递),大多数编译器使用普通调用方案,就像在 C 中一样。

【讨论】:

  • +1。 map' 4 是一个有一个参数的函数,它的类型为 Int -> Int
【解决方案5】:

这有帮助吗?

max' = \m -> \n -> if (m > n)
                       then m
                       else n

写成 lambdas。 max' 是一个 lambda 的值,它本身返回一个给定一些 m 的 lambda,它返回该值。

因此 max' 4 是

max' 4 = \n -> if (4 > n)
                   then 4
                   else n

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-04-04
    • 2012-09-21
    • 2013-06-03
    相关资源
    最近更新 更多