JavaScript函数式编程解释:部分应用和柯里化
介绍
随着 Redux JavaScript 库、Reason 语法扩展和工具链以及 Cycle JavaScript 框架的采用,使用 JavaScript 进行函数式编程变得越来越重要。 两个源于函数思想的重要思想是 currying,它将多个参数的函数转换为一系列函数调用,以及 partial application,它修复了函数的某些值没有完全评估函数的参数。 在本文中,我们将探讨这些想法的一些实例,并确定它们出现的一些可能会让您感到惊讶的地方。
阅读本文后,您将能够:
- 定义部分应用和柯里化并解释两者之间的区别。
- 使用部分应用程序来修复函数的参数。
- 便于局部应用的咖喱函数。
- 设计便于部分应用的功能。
没有部分应用的例子
与许多模式一样,部分应用在上下文中更容易理解。
考虑这个 buildUri 函数:
function buildUri (scheme, domain, path) {
return `${scheme}://${domain}/${path}`
}
我们这样称呼:
buildUri('https', 'twitter.com', 'favicon.ico')
这将产生字符串 https://twitter.com/favicon.ico。
如果您要构建大量 URL,这是一个方便的功能。 但是,如果您主要在 Web 上工作,则很少使用 http 或 https 以外的 scheme:
const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico')
const googleHome = buildUri('https', 'google.com', '')
请注意这两行之间的共同点:两者都将 https 作为初始参数传递。 我们宁愿去掉重复,写一些更像:
const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico')
有几种方法可以做到这一点。 让我们看看如何通过部分应用来实现它。
部分应用:固定参数
我们同意,而不是:
const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico')
我们更愿意写:
const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico')
从概念上讲,buildHttpsUri 与 buildUri 的 完全相同 ,但其 scheme 参数具有固定值。
我们可以像这样直接实现 buildHttpsUri:
function buildHttpsUri (domain, path) {
return `https://${domain}/${path}`
}
这将做我们想要的,但还没有完全解决我们的问题。 我们正在复制 buildUri,但将 https 硬编码为其 scheme 参数。
部分应用程序允许我们这样做,但是通过利用我们在 buildUri 中已有的代码。 首先,我们将了解如何使用名为 Ramda 的功能实用程序库来完成此操作。 然后,我们将尝试手动操作。
使用 Ramda
使用 Ramda,部分应用程序如下所示:
// Assuming we're in a node environment
const R = require('ramda')
// R.partial returns a new function (!)
const buildHttpsUri = R.partial(buildUri, ['https'])
之后,我们可以这样做:
const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico')
让我们分解这里发生的事情:
- 我们调用了 Ramda 的
partial函数,并传递了两个参数:第一个,一个名为buildUri的函数,第二个,一个包含一个"https"值的 array。 - Ramda 然后返回一个新函数,其行为类似于
buildUri,但将"https"作为其第一个参数。
在数组中传递更多值可修复更多参数:
// Bind `https` as first arg to `buildUri`, and `twitter.com` as second
const twitterPath = R.partial(buildUri, ['https', 'twitter.com'])
// Outputs: `https://twitter.com/favicon.ico`
const twitterFavicon = twitterPath('favicon.ico')
这允许我们通过为特殊情况配置它来重用我们在其他地方编写的通用代码。
手动部分应用
在实践中,当您需要使用部分应用程序时,您将使用 partial 之类的实用程序。 但是,为了说明起见,让我们自己尝试这样做。
让我们先看片段,然后再剖析。
// Line 0
function fixUriScheme (scheme) {
console.log(scheme)
return function buildUriWithProvidedScheme (domain, path) {
return buildUri(scheme, domain, path)
}
}
// Line 1
const buildHttpsUri = fixUriScheme('https')
// Outputs: `https://twitter.com/favicon.ico`
const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico')
让我们分解发生了什么。
- 在第 0 行,我们定义了一个名为
fixUriScheme的函数。 此函数接受scheme,并返回另一个函数。 - 在第 1 行,我们将调用
fixUriScheme('https')的结果保存到一个名为buildHttpsUri的变量中,它的行为与我们使用 Ramda 构建的版本完全相同。
我们的函数 fixUriScheme 接受一个值,并返回一个函数。 回想一下,这使它成为 高阶函数 或 HOF。 这个返回的函数只接受两个参数:domain 和 path。
请注意,当我们调用这个返回函数时,我们只显式传递了 domain 和 path,但它记住了我们在第 1 行传递的 scheme。 这是因为内部函数 buildUriWithProvidedScheme 可以访问其父函数范围内的所有值,即使在父函数返回之后也是如此。 这就是我们所说的闭包。
这概括了。 每当一个函数返回另一个函数时,返回的函数都可以访问在父函数范围内初始化的任何变量。 这是使用闭包封装状态的一个很好的例子。
我们可以使用带有方法的对象来做类似的事情:
class UriBuilder {
constructor (scheme) {
this.scheme = scheme
}
buildUri (domain, path) {
return `${this.scheme}://${domain}/${path}`
}
}
const httpsUriBuilder = new UriBuilder('https')
const twitterFavicon = httpsUriBuilder.buildUri('twitter.com', 'favicon.ico')
在此示例中,我们为 UriBuilder 类的每个实例配置一个特定的 scheme。 然后,我们可以调用 buildUri 方法,将用户想要的 domain 和 path 与我们预先配置的 scheme 结合起来,生成想要的 URL。
概括
回想一下我们开始的例子:
const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico')
const googleHome = buildUri('https', 'google.com', '')
让我们做一个小改动:
const twitterHome = buildUri('https', 'twitter.com', '')
const googleHome = buildUri('https', 'google.com', '')
这次有两个共同点:两种情况下的方案 "https" 和路径,这里是空字符串。
我们之前看到的 partial 函数部分适用于左侧。 Ramda 还提供了 partialRight,它允许我们从右到左进行部分应用。
const buildHomeUrl = R.partialRight(buildUri, [''])
const twitterHome = buildHomeUrl('https', 'twitter.com')
const googleHome = buildHomeUrl('https', 'google.com')
我们可以更进一步:
const buildHttpsHomeUrl = R.partial(buildHomeUrl, ['https'])
const twitterHome = buildHttpsHomeUrl('twitter.com')
const googleHome = buildHttpsHomeUrl('google.com')
设计考虑
要将 scheme 和 path 参数都修复为 buildUrl,我们必须首先使用 partialRight,然后在结果上使用 partial .
这并不理想。 如果我们可以使用 partial(或 partialRight),而不是按顺序使用两者会更好。
让我们看看我们能不能解决这个问题。 如果我们重新定义 buildUrl:
function buildUrl (scheme, path, domain) {
return `${scheme}://${domain}/${path}`
}
这个新版本传递了我们可能首先知道的值。 最后一个参数 domain 是我们最可能想要改变的参数。 按此顺序排列参数是一个很好的经验法则。
我们也可以只使用 partial:
const buildHttpsHomeUrl = R.partial(buildUrl, ['https', ''])
这让我们明白了论点顺序很重要。 有些订单比其他订单更方便部分应用。 如果您打算将函数与部分应用程序一起使用,请花时间考虑参数顺序。
咖喱和方便的局部应用
我们现在用不同的参数顺序重新定义了 buildUrl:
function buildUrl (scheme, path, domain) {
return `${scheme}://${domain}/${path}`
}
注意:
- 我们最有可能想要修正的论点出现在左边。 我们想要改变的是一直在右边。
buildUri是三个参数的函数。 换句话说,我们需要传递三个东西来让它运行。
我们可以使用一个策略来利用这一点:
const curriedBuildUrl = R.curry(buildUrl)
// We can fix the first argument...
const buildHttpsUrl = curriedBuildUrl('https')
const twitterFavicon = buildHttpsUrl('twitter.com', 'favicon.ico')
// ...Or fix both the first and second arguments...
const buildHomeHttpsUrl = curriedBuildUrl('https', '')
const twitterHome = buildHomeHttpsUrl('twitter.com')
// ...Or, pass everything all at once, if we have it
const httpTwitterFavicon = curriedBuildUrl('http', 'favicon.ico', 'twitter.com')
curry 函数接受一个函数 curries 它,并返回一个新函数,与 partial 不同。
Currying 是将我们一次调用的具有多个变量的函数(例如 buildUrl)转换为一系列函数调用的过程,其中我们一次传递一个变量。
curry不会立即修复参数。 返回的函数采用与原始函数一样多的参数。- 如果将所有必要的参数传递给 curried 函数,它的行为类似于
buildUri。 - 如果您传递的参数少于原始函数所用的参数,则柯里化函数将自动返回您通过调用
partial获得的相同内容。
柯里化为我们提供了两全其美:自动部分应用和使用我们原始函数的能力。
请注意,currying 可以更轻松地创建我们函数的部分应用版本。 这是因为柯里化函数很容易部分应用,只要我们小心我们的参数顺序。
我们可以像调用 buildUri 一样调用 curriedbuildUrl:
const curriedBuildUrl = R.curry(buildUrl)
// Outputs: `https://twitter.com/favicon.ico`
curriedBuildUrl('https', 'favicon.ico', 'twitter.com')
我们也可以这样称呼它:
curriedBuildUrl('https')('favicon.ico')('twitter.com')
请注意, curriedBuildUrl('https') 返回一个函数,其行为类似于 buildUrl,但其方案固定为 "https" 。
然后,我们立即用 "favicon.ico" 调用这个函数。 这将返回另一个函数,其行为类似于 buildUrl,但其方案固定为 "https",其路径固定为空字符串。
最后,我们用 "twitter.com" 调用这个函数。 由于这是最后一个参数,因此函数解析为最终值:http://twitter.com/favicon.ico。
重要的一点是:curriedBuldUrl 可以作为一系列函数调用进行调用,每次调用只传递一个参数。 它是将一个包含许多“一次性”传递的变量的函数转换为我们称之为柯里化的“单参数调用”序列的过程。
结论
让我们回顾一下主要内容:
- 部分应用允许我们修复函数的参数。 这让我们可以从其他更通用的函数中派生出具有特定行为的新函数。
- Currying 将一个“同时”接受多个参数的函数转换为一系列函数调用,每个函数调用一次只涉及一个参数。 具有精心设计的参数顺序的柯里化函数便于部分应用。
- Ramda 提供
partial、partialRight和curry实用程序。 类似的流行库包括 Underscore 和 Lodash。