在JavaScript和函数组合中使用纯函数

Today, I'd like to share some thoughts on two fundamental concepts in functional programming: Pure functions and function composition.

今天,我想就函数式编程中的两个基本概念分享一些想法:纯函数和函数组合。

After reading, you'll be able to:

阅读后,您将能够:

  • Write pure functions, and explain basic advantages of purity

    编写纯函数,并解释纯的基本优点
  • Isolate impure and pure operations in your JavaScript

    隔离JavaScript中不纯净的操作

Understanding purity is an important prerequisite to cultivating a functional mindset—I'd go so far as to say it's the prerequisite to understanding functional programming, period.

理解纯净度是培养功能思维方式的重要先决条件,我什至可以说,这是理解功能编程时期的先决条件。

So: Let's talk purity.

所以:让我们谈谈纯度。

纯函数 ( Pure Functions )

Two things need to be true for a function to be pure.

要使函数纯净,必须满足两件事。

First condition: A function can be pure if and only if the only thing it uses to calculate its output are the arguments you pass it, and local variables declared inside of the function itself.

第一个条件 :一个函数只有当您用来计算其输出的唯一参数是传递给它的参数以及在函数本身内部声明的局部变量时,才可以是函数。

As usual, this is clearer by example than explanation.

像往常一样,通过示例比解释更清楚。

function greetUser (user, greeting='Hello') {
  return `${greeting}, ${user.firstName} ${user.lastName}!`
}

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

greetUser(joe) // 'Hello, Joe Schmoe!'

Note that the only thing greetUser uses to calculate its return value is the user object we pass as argument.

请注意, greetUser用来计算其返回值的唯一内容是我们作为参数传递的user对象。

Second condition: A function can be pure if and only if it only if it does not change state outside of its own scope.

第二个条件 :一个函数只有且仅当它不改变其自身作用域之外的状态时,才可以是纯函数。

The greetUser function above satisfies this condition: It takes a user, then returns a string. It doesn't touch state at all.

上面的greetUser函数满足以下条件:接受一个user ,然后返回一个字符串。 它根本不涉及状态。

Let's see an example where this doesn't hold true.

让我们看一个不成立的例子。

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

function impureUpdate () {
  joe.firstName = 'JOE'
}

joe.firstName // 'Joe'

// A
unsafeUpdate() // changes `joe`'s `firstName` property

// B
joe.firstName // 'JOE'

Note that joe is declared in the global scope—not in the local scope of impureUpdate. Yet, calling impureUpdate (B) changes the value of joe.firstName.(C).

请注意, joe是在全局范围内声明的,而不是在impureUpdate的局部范围内impureUpdate 。 但是,调用impureUpdate (B)会更改joe.firstName 。(C)的值。

Thus, we see that calling impureUpdate changes state outside of the function's execution environment. These changes persist after the function has returned.

因此,我们看到调用impureUpdate更改函数执行环境之外的状态。 函数返回 ,这些更改仍然存在。

If a function does change state outside of its scope, as does unsafeUpdate, it is said to have side effects. So, another, more common way of stating this condition is: A function can only be pure if it has no side effects.

如果函数确实在其范围之外更改了状态,那么unsafeUpdate发生副作用 。 因此,另一种表示这种情况的更常见方法是:一个函数只有在没有副作用的情况下才能是纯函数。

By the way, side effects include things like I/O and printing to the console. So, anything involving console.log, fs.readFile, etc., is technically "impure". But don't worry about that; we'll loop back to this little kink soon.

顺便说一句,副作用包括I / O和打印到控制台等内容。 因此,涉及console.logfs.readFile等的任何内容在技术上都是“不纯净的”。 但是不用担心。 我们将很快回到这个小问题。

To sum up, then, a function f is pure if and only if it meets these two conditions:

总而言之,函数f仅在满足以下两个条件时才是纯函数:

  1. f uses only its arguments and local variables to compute its result; and

    f 使用其参数和局部变量来计算其结果; 和
  2. f has no side effects.

    f没有副作用。

Let's talk about why we should care, and then take a look at how writing pure functions.

让我们谈谈我们为什么要关心,然后看看如何编写纯函数。

纯度的优点:或者杂质的缺点 (Advantages of Purity: Or, Disadvantages of Impurity)

The logical next question is: So what?

逻辑上的下一个问题是: 那又如何?

For us, the most important reasons this matters are:

对我们来说,最重要的原因是:

  1. Pure functions are "predictable", and thus easy to test.

    纯函数是“可预测的”,因此易于测试。
  2. Pure functions simplify state management.

    纯功能简化了状态管理。

...Predictably, there are a lot of other advantages to purity, but we'll focus on these. rDrop your favorite in the comments if I missed it.

……可以预期,纯度还有很多其他优点,但我们将重点关注这些优点。 r如果我错过了,请在评论中放弃您的最爱。

纯函数是可预测的 (Pure Functions are Predictable)

Recall the two conditions from above.

从上面回顾两个条件。

A function is pure if it:

如果函数是纯函数,则它是:

  1. Uses only its arguments and local variables to compute its result; and

    使用其参数和局部变量来计算其结果; 和
  1. Has no side effects.

    没有副作用。

Let's rephrase that.

让我们改一下。

A function is pure if it:

如果函数是纯函数,则它是:

  1. Does not use non-local variables or outside state to compute its result; and

    使用非局部变量或外部状态来计算其结果; 和
  2. Does not change external state as it runs.

    运行时更改外部状态。

This helps clarify what we mean by "predictable". Pure functions are predictable, in the sense that they will always return the same output for a given input.

这有助于弄清“可预测”的含义。 纯函数是可预测的,从某种意义上说,对于给定的输入,纯函数将始终返回相同的输出。

Examples help. First, our pure function from above.

实例帮助。 首先,我们从上面的功能。

function greetUser (user, greeting='Hello') {
  return `${greeting}, ${user.firstName} ${user.lastName}!`
}

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

// This is boring...Same. Thing. Every. Time.
greetUser(joe) // 'Hello, Joe Schmoe!'
greetUser(joe) // 'Hello, Joe Schmoe!'
greetUser(joe) // 'Hello, Joe Schmoe!'

If we pass joe as its argument, the function greetUser will always return 'Hello, Joe Schmoe!'...Until the end of time.

如果我们将joe作为其参数传递,则函数greetUser始终返回'Hello, Joe Schmoe!' ...直到最后一刻。

As you'd probably guess, not all functions are "predictable" in this way.

您可能会猜到,并非所有函数都可以通过这种方式“可预测”。

function greetUserWithTime (user, greeting='Hello') {
  const salutation =`${greeting}, ${user.firstName} ${user.lastName}!`
  const currentTime = `The current time is: ${Date()}.`

  return `${salutation} ${currentTime}`
}

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}



// Now, the result is different on every call
greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 16:59:11 GMT-0500 (Eastern Standard Time)'

greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 17:02:11 GMT-0500 (Eastern Standard Time)'

greetUserWithTime(joe) // 'Hello, Joe Schmoe! The current time is: Sun Jan 28 2018 17:04:16 GMT-0500 (Eastern Standard Time)'

Notice that, this time around, our function returns a different result each time we call it. It does this in spite of the fact that we pass the same argument upon each invocation.

注意,这一次,函数每次调用时都会返回不同的结果。 尽管我们在每次调用时都传递了相同的参数,但它还是这样做了。

The reason we get different return values is because, instead of relying only on local variables and arguments, greetUserWithTime relies on external state—namely, the current time at the moment the function is called.

我们获得不同的返回值的原因是, greetUserWithTime依赖于外部状态,而不是仅依赖于局部变量和参数,即,函数被调用的当前时间

Since the time changes each time we call greetUserWithTime, its return value does, as well. This makes it "impure", and unpredictable: To know what it will return ,we need to know both the arguments we pass; and the time of invocation.

由于每次我们调用greetUserWithTime时时间都会改变,因此它的返回值也会改变。 这使得它“不精确”且不可预测:要知道它将返回什么,我们需要知道我们传递的两个参数; 调用时间。

This is one of the major disadvantages of impure functions. To understand what they do, we need to know which pieces of system state influence their output. To make things worse, we need to know which pieces of the system influence those pieces of the system...And so on.

这是不纯功能的主要缺点之一。 要了解它们的作用,我们需要知道哪些系统状态会影响其输出。 更糟糕的是,我们需要知道系统的哪些部分会影响系统的那些部分……等等。

A pure function's output, however, is determined solely by its inputs. This makes it "predictable", because its outputs are stable: In other words, the return value it generates for a given input will never change. All we need to know to understand a pure function is how it manipulates the data we pass it—information about the rest of the program is irrelevant.

但是,纯函数的输出仅由其输入确定。 这使其“可预测”,因为其输出是稳定的 :换句话说,它为给定输入生成的返回值将永远不会改变。 我们需要了解的一个纯函数就是如何处理传递给它的数据-有关程序其余部分的信息是无关紧要的。

The fact that pure functions are predictable means they're easy to test.

纯函数是可预测的事实意味着它们易于测试。

Consider greetUser. It's easy to predict what this function should output for any given user object. A (very simple) test might look as follows:

考虑greetUser 。 很容易预测此函数应为任何给定的用户对象输出什么。 (非常简单的)测试可能如下所示:

const joe = {
  firstName: 'Joe',
  lastName: 'Schmoe'
}

const expected = 'Hello, Joe Schmoe!'
const actual = greetUser(joe)

if (expected === actual) {
  console.log('All good.')
} else {
  console.error('ERROR. Expected: ${expected}...But got: ${actual}.`
}

A full test suite would address edge cases, etc., but the concept is clear: Expected behavior is easy to verify.

完整的测试套件可解决极端情况等,但概念很明确:预期行为易于验证。

Contrast this with greetUserWithTime. It's immediately unclear how to handle the fact that its output will be different every time. One standard solution is to mock the system time—i.e., "rig" the test environment such that Date() returns a value you specify—but that's a lot of complexity for an otherwise trivial task.

将此与greetUserWithTime对比。 现在还不清楚如何处理每次输出都会不同的事实。 一种标准的解决方案是模拟系统时间(即“绑定”测试环境,以使Date()返回您指定的值Date() ,但这对于其他琐碎的任务来说非常复杂。

The fact that pure functions neither influence nor rely upon external state means they simplify state management—we don't have to think about calling a pure function will affect the state of other components of our program, because they don't affect them at all.

纯函数既不影响也不依赖于外部状态这一事实意味着它们简化了状态管理-我们不必考虑调用纯函数将影响程序其他组件的状态, 因为它们根本不影响它们。

处理杂质 ( Managing Impurity )

Here are things you can't do when writing pure functions:

编写纯函数时,您不能做以下事情:

  1. Modify global state. This one is fairly obvious from the above examples.

    修改全局状态。 从以上示例中可以明显看出这一点。
  2. I/O. Reading/writing files is a side effect, so it's not allowed in pure functions. Logging to the console is also a side effect. Yes, that means pure functions can't contain console.log statements.

    I / O。 读取/写入文件是一个副作用,因此纯函数不允许这样做。 登录到控制台也是一个副作用。 是的,这意味着纯函数不能包含console.log语句。
  3. AJAX. A network request to the same API endpoint could result in either the data you asked for, or an error.

    AJAX。 对相同API端点的网络请求可能会导致您要求的数据错误。

That restriction on I/O and AJAX seems limiting. But, keep in mind—there's nothing wrong with "impure" functions (hence the quotes). It's just that using them promiscuously can make programs more difficult to understand.

对I / O和AJAX的限制似乎是有限的。 但是,请记住-“不纯”功能没有 (因此引号)。 只是滥用它们会使程序更难以理解。

The general rule of thumb is to manipulate data using only pure functions; and then render the results of those manipulations in impure functions.

一般的经验法则是使用纯函数来操作数据。 然后在不纯函数中呈现这些操作的结果。

In other words, rather than strive to write only pure functions—which is impossible—we instead impose the discipline of separating pure and impure operations.

换句话说,我们不是努力只编写纯函数(这是不可能的),而是强加了将纯操作和不纯操作分开的原则。

Developing a sense for where impurity occurs in your code is a crucial step towards developing a functional awareness.

对代码中出现杂质的地方进行感知是开发功能意识的关键步骤。

隔离不纯函数 (Isolating Impure Functions)

Let's reiterate a reasonable approach to designing with pure functions:

让我们重申一种使用纯函数进行设计的合理方法:

  1. Before coding, identify what data you need. The user model? Database entries for a customer's recent bank transactions?

    编码之前,请确定您需要哪些数据。 user模型? 客户最近的银行交易的数据库条目?
  2. Identify the source of your data, and load it. This will generally involve "impurity" in the form of I/O, AJAX, or monitoring of user interaction.

    确定数据源并加载。 这通常会涉及I / O,AJAX形式或监视用户交互的“杂质”。
  3. Transform the data into the form your app requires. This might involve extracting and combining only the pieces of data you need, such as the firstName and lastName properties in greetUser above; deriving values from it, such as a customer's average spending over the past month; etc. Ideally, this should involve pure functions.

    将数据转换为您的应用程序所需的形式。 这可能涉及仅提取和组合所需的数据,例如上面greetUserfirstNamelastName属性。 从中得出价值,例如客户过去一个月的平均支出; 理想情况下,这应该包含纯函数。
  4. Identify the parts of your code that involve "impurity"—I/O, AJAX, rendering to the DOM, etc.—and write a function that encapsulates the side effects. This function will obviously not be pure. Pass the data it needs as an argument—emphatically, do not manipulate the data inside this function.

    识别代码中涉及“杂质”的部分(I / O,AJAX,渲染到DOM等),并编写一个封装副作用的函数。 该功能显然不是纯函数。 它传递-emphatically需要作为参数数据, 不会操作这个功能里面的数据。

Let's apply these steps to developing a simple UI that dumps a random GIF on the page based on a user-entered search value.

让我们将这些步骤应用于开发一个简单的UI,该UI根据用户输入的搜索值在页面上转储随机GIF。

I'd initially done this in React, but idiomatic usage of this.setState confused the point. So, here it is in vanilla JS, instead.

我最初是在React中完成的,但是this.setState惯用用法使这一点感到困惑。 因此,这里是在普通JS中。

Before we get started, let's initialize some of top-level variables we'll be dealing with.

在开始之前,让我们初始化一些将要处理的顶级变量。

'use strict';

const image = document.querySelector('#image')
const refresh = document.querySelector('#refresh')
const input = document.querySelector('#search-term')

const state = {
  searchTerm: 'surprise'
}

Here, we're fetching:

在这里,我们正在获取:

  • The image tag, where we'll dump our GIF

    image标签,我们将在其中转储GIF
  • The refresh button, which resets the image; and

    refresh按钮,用于重置图像; 和
  • The input element, where users will enter their query.

    input元素,用户将在其中输入查询。

We've also created a global state object, where we'll store our searchTerm.

我们还创建了一个全局state对象,将在其中存储我们的searchTerm

识别数据 (Identifying the Data)

We need two pieces of data to make this work:

我们需要两个数据来完成这项工作:

  1. A user-entered query

    用户输入的查询
  2. A URL for a GIF

    GIF的网址

We'll use the user-entered query to make an AJAX call to the GIPHY API. We'll use the URL provided in the response to set the src tag of an img attribute on the page.

我们将使用用户输入的查询来对GIPHY API进行AJAX调用。 我们将使用响应中提供的URL在页面上设置img属性的src标签。

Neither function will be pure, as they depend on external state.

这两个函数都不是纯函数,因为它们取决于外部状态。

检索数据 (Retrieving the Data)

The next step would be to implement functions for retrieving the user-entered query, and sending the AJAX request.

下一步将是实现用于检索用户输入的查询并发送AJAX请求的功能。

The simplest way to capture user input is, of course, to set a listener on the input which updates the state on keyup.

捕获用户输入的最简单方法当然是在input上设置侦听器,以更新keyup上的状态。

The simplest way to send the initial AJAX request is with fetch (or your favorite AJAX utility). We'll also create a (pure) buildUrl function for putting together the GIPHY query.

发送初始AJAX请求的最简单方法是使用fetch (或您最喜欢的AJAX实用程序)。 我们还将创建一个(纯) buildUrl函数来组合GIPHY查询。

// @impure: handler has side-effects
input.addeventlistener('keyup', function keyuphandler (event) {
  setsearchterm(event.target.value)
})

// @impure: dom manipulation
refresh.addeventlistener('click', function clickhandler (event) {
  updateimage(state.searchterm, image)
})

// @impure: has side-effects (updates global state)
const setsearchterm = searchterm => {
  state.searchterm = searchterm
}

// @pure
const buildurl = searchterm => {
  const url='http://api.giphy.com/v1/gifs/search'
  const apikey = 'fthi8mvnpuogqtuwmugzbdherk0etrqq'

  return `${url}?q=${searchterm}&api_key=${apikey}`
}

// @impure: time-dependen--whether we receive an error response or data depends on network
const fetchurl = url => fetch(url)

处理数据 (Manipulating the Data)

Our fetchGif function will return a Promise, which will resolve with a response (or error, which we'll ignore for now).

我们的fetchGif函数将返回一个Promise,它将通过响应(或错误,我们fetchGif将其忽略)解决。

That response will contain a bunch of information provided by the API. We only need one piece: the URL for the GIF.

该响应将包含API提供的一堆信息。 我们只需要一件:GIF的URL。

In this case, the only manipulation we need to do is pluck the value of this one property, and throw away the rest.

在这种情况下,我们唯一需要做的操作就是选择该一个属性的值,然后丢弃其余的属性。

// @pure
const pluckImageUrl = gif => gif.images.original.url

This pluckImageUrl function returns the URl we'll need to use to update our image's src attribute.

pluckImageUrl函数返回URl,我们需要用它来更新imagesrc属性。

更新DOM (Updating DOM)

The last thing to do is update the value of our image tag's src attribute.

最后要做的是更新image标签的src属性的值。

We'll do this within the then clauses following fetchUrl. We've also created a (pure) randomIdex function, which allows us to pick a random image from the list of GIFs provided by the GIPHY API.

我们将在fetchUrl then子句中执行此fetchUrl 。 我们还创建了一个(纯) randomIdex函数,该函数使我们能够从GIPHY API提供的GIF列表中选择随机图像。

// @pure
const randomIndex = ({length}) => Math.floor(Math.random() * length)

// @impure: Performs AJAX, updates DOM
const updateImage = (searchTerm, image) => {
  fetchUrl(buildUrl(searchTerm))
    .then(response => response.json())
    .then(data => {
      const gifList = data.data
      const index = randomIndex(gifList)

      const gif = gifList[index]
      image.src = gif.images.original.url
    })
    .catch(error => console.error(error))
}

...And that's it. You can clone the repo and open index.html to run it if you'd like.

...就是这样。 您可以克隆存储库,然后打开index.html来运行它

The point, here, is that separating pure and impure functions helps make things more readable, more modular, and more testable. These advantages are less apparent in this example than they are in larger codebases, but the potential improvements to readability and modularity should be apaprent.

这里的要点是,将纯函数和不纯函数分开有助于使事情更易读,更模块化和更可测试。 与大型代码库相比,这些优点在本示例中不那么明显,但是对可读性和模块化的潜在改进应该是适当的。

Note that extracting pure functions and using them in functions like updateImage makes it easier to isolate bugs. I often see students approach problems like this by setting a click handler on the reresh button, then fetching the user's search term; building the URL; sending the AJAX request; and updating the DOM, all in that handler.

请注意,提取纯函数并在诸如updateImage类的函数中使用它们可以更轻松地隔离错误。 我经常看到学生通过在刷新按钮上设置点击处理程序,然后获取用户的搜索字词来解决此类问题。 建立网址; 发送AJAX请求; 并在该处理程序中更新DOM。

This usually ends up being cumbersome to debug, even in simple cases: If things don't work, it's not obvious which of the four pieces is broken. Writing four functions, each of which does one thing, makes it easier to identify exactly which piece is broken.

即使在简单的情况下,这通常也很麻烦调试:如果事情不起作用,则不清楚这四个部分中的哪一个已损坏。 编写四个函数,每个函数都做件事,使您更容易准确地确定哪一块断了。

摘要 ( Summary )

And there we have it: Purity, in a nutshell. Let's review the high points.

简而言之,就是这里。 让我们回顾一下重点。

  • A function has side effects if it changes external state.

    如果函数改变外部状态,则会产生副作用
  • A function is pure if 1) its output is determined soley by its input values, and 2) it has no observable side effects.

    一个函数是纯函数,如果1)它的输出由其输入值确定,并且2)它没有可观察到的副作用。
  • Pure functions cannot perform I/O, AJAX, DOM manipulations, or logging.

    纯函数无法执行I / O,AJAX,DOM操作或日志记录。
  • Pure functions are (generally) easier to test than those that are dependent on external state.

    纯函数(通常)比依赖于外部状态的函数更容易测试。
  • "Impure" functions aren't intrinsically bad—hey're just potentially confusing.

    “不纯正”的功能本质上并没有坏处-只是潜在地造成混淆。
  • Isolating pure and impure operations is good practice.

    隔离纯净和不纯净的操作是一种好习惯。

Turns out, there are much deeper reasons that purity is compelling:

事实证明,令人信服的深层原因有很多:

  • Parallelizing the execution of pure functions is (relatively) easy: Since they don't modify external state, they don't experience race conditions due to shared state.

    并行执行纯函数很容易(相对):由于它们不修改外部状态,因此由于共享状态而不会遇到竞争条件
  • Because a pure function's return value is determined completely by its inputs, they're easily memoized. If you're not familiar with memoization, it's worth reading about.

    由于纯函数的返回值完全由其输入决定,因此很容易记住它们。 如果您不熟悉记忆, 则值得阅读
  • Because pure functions are, in a sense, well-behaved, compilers can make assumptions about their behavior that they can't make about functions with side effects. This allows them to make optimizations that would be impossible in the absence of purity, such as data structure fusion.

    因为从某种意义上说纯函数的行为良好,所以编译器可以对自己的行为进行假设,而他们无法做出具有副作用的函数。 这使他们能够进行优化,而这种优化在缺乏纯度的情况下是不可能的,例如数据结构融合

These are all advanced concepts relatively uncommon in the world of JavaScript, but they do serve to illustrate just how powerful purity can be.

这些都是在JavaScript世界中相对罕见的高级概念,但它们的确可以说明纯度有多么强大。

Now that we've gotten this prerequisite out of the way, we can do some heavy lifting. Next time, we'll dive into function composition, and bring at least one of these advanced concepts—fusion—into our everyday JavaScript.

现在我们已经解决了这个先决条件,我们可以进行一些繁重的工作。 下次,我们将深入研究功能组合,并将至少一种高级概念(融合)引入我们的日常JavaScript中。

Stay tuned.

敬请关注。

翻译自: https://scotch.io/tutorials/wielding-pure-functions-in-javascript-and-function-composition

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值