JavaScript 函数式编程详解:部分应用和咖喱

介绍

随着 Redux JavaScript 库的采用, Reason语法扩展和工具链,以及 Cycle JavaScript 框架,使用 JavaScript 的功能编程变得越来越重要。在功能思维中有根源的两个重要想法是 currying,它将多个论点的函数转化为一系列函数调用,以及 partial application,它可以固定函数的某些论点的价值,而不完全评估函数。

读完这篇文章后,你将能够:

  • 定义部分应用和库里,并解释两者之间的区别
  • 使用部分应用来对函数进行定义
  • 库里函数来促进部分应用
  • 设计函数来促进部分应用

没有部分应用的例子

与许多模式一样,部分应用更容易理解。

考虑这个buildUri函数:

1function buildUri (scheme, domain, path) {
2  return `${scheme}://${domain}/${path}`
3}

我们这样称呼:

1buildUri('https', 'twitter.com', 'favicon.ico')

这会产生字符串 https://twitter.com/favicon.ico

但是,如果你主要在网络上工作,你很少使用方案,而不是httphttps:

1const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico')
2const googleHome = buildUri('https', 'google.com', '')

注意这两个行之间的共同之处: 两者都通过https作为初始论点. 我们更愿意切断重复并写一些更像这样的东西:

1const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico')

有几种方法可以做到这一点,让我们看看如何通过部分应用来实现。

部分应用:固定论点

我们同意这一点,而不是:

1const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico')

我们宁愿写:

1const twitterFavicon =  buildHttpsUri('twitter.com', 'favicon.ico')

从概念上讲,buildHttpsUri 与buildUri 完全相同,但其方案参数具有固定的值。

我们可以直接实现buildHttpsUri,如下:

1function buildHttpsUri (domain, path) {
2  return `https://${domain}/${path}`
3}

这将做我们想要的,但还没有完全解决我们的问题. 我们重复了buildUri,但硬编码了https作为它的方案论点。

部分应用允许我们做到这一点,但通过利用我们在buildUri中已经拥有的代码。首先,我们将看到如何使用一个名为Ramda(LINK0)的功能工具库来做到这一点。

使用 Ramda

使用 Ramda,部分应用程序看起来像这样:

1// Assuming we're in a node environment
2const R = require('ramda')
3
4// R.partial returns a new function (!)
5const buildHttpsUri = R.partial(buildUri, ['https'])

之后,我们可以做:

1const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico')

让我们把这里发生的事情分解下来:

  • 我们调用了Ramda的部分函数,并通过了两个参数:第一,一个函数,称为buildUri,第二,一个包含一个https值的 array

在数组中传递更多值可修复进一步的参数:

1// Bind `https` as first arg to `buildUri`, and `twitter.com` as second
2const twitterPath = R.partial(buildUri, ['https', 'twitter.com'])
3
4// Outputs: `https://twitter.com/favicon.ico`
5const twitterFavicon = twitterPath('favicon.ico')

这使我们能够重复使用我们在其他地方编写的通用代码,将其配置为特殊案例。

手动部分应用

在实践中,当你需要使用部分应用时,你会使用部分这样的实用工具,但为了说明,让我们试着自己做。

让我们先看看片段,然后分解。

 1// Line 0
 2function fixUriScheme (scheme) {
 3  console.log(scheme)
 4  return function buildUriWithProvidedScheme (domain, path) {
 5    return buildUri(scheme, domain, path)
 6  }
 7}
 8
 9// Line 1
10const buildHttpsUri = fixUriScheme('https')
11
12// Outputs: `https://twitter.com/favicon.ico`
13const twitterFavicon = buildHttpsUri('twitter.com', 'favicon.ico')

让我们打破发生了什么。

  • 在 0 行,我们定义了一个名为 fixUriScheme 的函数. 这个函数接受一个 scheme,并返回另一个函数.
  • 在 1 行,我们将调用 fixUriScheme('https') 的结果保存到一个名为 buildHttpsUri 的变量中,该函数与我们用 Ramda 构建的版本完全相同

我们的函数 fixUriScheme 接受一个值,然后返回一个函数. 请记住,这使得它成为一个 高级函数,或 HOF. 这个返回的函数只接受两个参数: 路径

请注意,当我们称呼这个返回函数时,我们只会明确地传递路径,但它会记住我们在线上传递的方案,这是因为内部函数buildUriWithProvidedScheme可以访问其母函数范围内的所有值,即使在母函数返回后。

每当一个函数返回另一个函数时,返回的函数可以访问在母函数范围内初始化的任何变量。

我们可以使用一种用方法的对象做类似的事情:

 1class UriBuilder {
 2
 3  constructor (scheme) {
 4    this.scheme = scheme
 5  }
 6
 7  buildUri (domain, path) {
 8    return `${this.scheme}://${domain}/${path}`
 9  }
10}
11
12const httpsUriBuilder = new UriBuilder('https')
13
14const twitterFavicon = httpsUriBuilder.buildUri('twitter.com', 'favicon.ico')

在本示例中,我们将UriBuilder类的每个实例配置为一个特定的方案,然后,我们可以调用buildUri方法,该方法将用户所需的路径与我们预先配置的方案相结合,以产生所需的URL。

一般化

回想一下我们开始的例子:

1const twitterFavicon = buildUri('https', 'twitter.com', 'favicon.ico')
2
3const googleHome = buildUri('https', 'google.com', '')

让我们来做一个小小的改变:

1const twitterHome = buildUri('https', 'twitter.com', '')
2
3const googleHome = buildUri('https', 'google.com', '')

这次有两个共同点:方案,https在两种情况下,以及路径,这里是空串。

我們之前看到的「部分」函數部分適用於左側. Ramda 還提供 partialRight,讓我們可以部分適用於右側。

1const buildHomeUrl = R.partialRight(buildUri, [''])
2
3const twitterHome = buildHomeUrl('https', 'twitter.com')
4const googleHome = buildHomeUrl('https', 'google.com')

我们可以进一步采取这一点:

1const buildHttpsHomeUrl = R.partial(buildHomeUrl, ['https'])
2
3const twitterHome = buildHttpsHomeUrl('twitter.com')
4const googleHome = buildHttpsHomeUrl('google.com')

设计考虑

要将方案路径论点都修复为buildUrl,我们必须先使用partialRight,然后在结果上使用partial

最好是我们可以使用部分(或部分权利),而不是连续使用两者。

让我们看看我们能否修复这个问题,如果我们重新定义‘buildUrl’:

1function buildUrl (scheme, path, domain) {
2  return `${scheme}://${domain}/${path}`
3}

这个新版本通过了我们最有可能提前知道的值。最后一个参数,‘域’,是我们最有可能想要改变的参数。

我们也可以使用部分:

1const buildHttpsHomeUrl = R.partial(buildUrl, ['https', ''])

这引导回归论点,论点顺序重要. 某些命令比其他命令更方便的部分应用. 花时间考虑论点顺序,如果你打算使用你的函数与部分应用。

和方便的部分应用

现在我们重新定义了「buildUrl」以不同的参数顺序:

1function buildUrl (scheme, path, domain) {
2
3  return `${scheme}://${domain}/${path}`
4
5}

注意这:

  • 我们最有可能想要修复的论点出现在左边,我们想要改变的论点是右边
  • buildUri 是由三个论点组成的函数,换句话说,我们需要通过三件事才能使它运行

有一个策略,我们可以利用这一点:

 1const curriedBuildUrl = R.curry(buildUrl)
 2
 3// We can fix the first argument...
 4const buildHttpsUrl = curriedBuildUrl('https')
 5const twitterFavicon = buildHttpsUrl('twitter.com', 'favicon.ico')
 6
 7// ...Or fix both the first and second arguments...
 8const buildHomeHttpsUrl = curriedBuildUrl('https', '')
 9const twitterHome = buildHomeHttpsUrl('twitter.com')
10
11// ...Or, pass everything all at once, if we have it
12const httpTwitterFavicon = curriedBuildUrl('http', 'favicon.ico', 'twitter.com')

curry函数采取一个函数, curries 它,并返回一个新的函数,而不是部分

_Currying 是将一个函数,我们同时称之为多个变量,如buildUrl,转化为一系列函数调用,我们一次传递每个变量。

返回的函数需要和原始函数一样多的参数

  • 如果您将所有必要的参数传递给 curried 函数,它将表现为 buildUri
  • 如果您传递的参数比原始函数少, curried 函数将自动返回您通过调用 partial 获得的相同的东西。

Currying给了我们两个世界的最好:自动部分应用和使用我们的原始功能的能力。

请注意,currying 使创建我们函数的部分应用版本更容易,这是因为 curried 函数方便地部分应用,只要我们小心我们的论点排序。

我们可以像我们称之为buildUri那样称呼curriedbuildUrl:

1const curriedBuildUrl = R.curry(buildUrl)
2
3// Outputs: `https://twitter.com/favicon.ico`
4curriedBuildUrl('https', 'favicon.ico', 'twitter.com')

我们也可以这样称呼它:

1curriedBuildUrl('https')('favicon.ico')('twitter.com')

请注意,curriedBuildUrl(https)返回一个函数,该函数行为像buildUrl,但其方案固定为https`。

然后,我们立即将这个函数称为favicon.ico。这将返回另一个函数,该函数行为像buildUrl,但其方案固定为https,其路径固定为空串。

最后,我们用twitter.com调用这个函数,因为这是最后一个参数,所以该函数解决到最后一个值:http://twitter.com/favicon.ico。

curriedBuldUrl可以称为函数呼叫的序列,其中我们每次呼叫只传递一个论点,这是将许多变量的函数转换为一次到我们称之为currying的一个论点呼叫的序列的过程。

结论

让我们回顾一下主要的 takeaways:

  • 部分应用允许我们修复函数的论点。这使我们能够从其他更一般的函数中提取新的函数,具有特定的行为
  • Currying将一函数转化为一系列函数调用,每个函数只涉及一次一个论点。具有精心设计的论点顺序的学术函数方便地部分应用
  • Ramda提供部分partialRightcurry实用程序。
Published At
Categories with 技术
comments powered by Disqus