惰性求值,可组合和模块化的JavaScript

ECMAScript 6, or the 6th edition of ECMA-262 standard, gives JavaScript developers new tools for writing more succinct and modular code. In this article, I’ll cover how we can use four of these features – iterables, generators, fat arrows, and for-of – in conjunction with higher-order functions, function composition, and lazy evaluation, to write cleaner and more modular JavaScript.

Before we dive in to a larger example, we’ll review some more general concepts.

CMAScript 6或第6版ECMA-262标准为JavaScript开发人员提供了编写更简洁和模块化代码的新工具。在本文中,我将介绍如何使用四个这些功能 - 迭代,生成器,胖箭头和for-of - 结合高阶函数,函数组合和懒惰评估来编写更干净,更模块化JavaScript的。
在我们再来一个更大的例子之前,我们将再来看一些更一般的概念。

Higher-order functions高阶功能

A higher-order function is a normal function that satisfies at least one of the following conditions:

  1. It accepts one or more functions as parameters
  2. It returns a function

If you’ve ever written an event listener or used Array.prototype.map, then you’ve used a higher-order function. Higher-order functions promote reusability and modularity by decoupling how an operation is applied from the operation itself.

For example, the function we pass to Array.prototype.map has no knowledge of collections or how to operate on them. All it knows is how to operate on a value. Therefore it can be reused in the context of both single values and collections.

高阶函数是满足以下条件中的至少一个的正常函数:
它接受一个或多个函数作为参数
它返回一个函数
如果你曾经写过一个事件侦听器或者使用了Array.prototype.map,那么你使用了一个更高阶的函数。高阶功能通过解除操作本身的操作方式来提升可重用性和模块性。
例如,我们传递给Array.prototype.map的函数不知道集合或如何操作它们。所有它知道的是如何操作一个价值。因此,它可以在单个值和集合的上下文中重用。

Function composition功能组成

Function composition is the combination of simple functions to build more complicated ones. Given two functions f and gcompose(f, g) gives us a new function that returns f(g(x)). Also, since the result of composition is a function, it can be further composed or be passed to higher-order functions.

Let’s look at an example. We are tasked with writing a program that takes a file as input and returns an array of the number of vowel occurrences in each word on each line. There are many approaches to this problem. One such approach is to write a single big function that does it all.

The monolithic solution below uses the new for-of construct to loop over values in an iterable instead of the usual for loop. An iterable is a container that implements the iterator protocol and can yield values one by one (example: arrays, strings, generators, etc.).

功能组合是简单功能的组合,构建更复杂的功能。给定两个函数f和g,comp(f,g)给出了一个返回f(g(x))的新函数。此外,由于组合的结果是功能,所以可以进一步组合或传递到高阶函数。
我们来看一个例子。我们的任务是编写一个文件作为输入的程序,并返回每一行中每个单词中的元音数量的数组。这个问题有很多方法。一个这样的方法是写一个大的功能,这样做。
下面的单片解决方案使用新的for-of构造来循环迭代而不是通常的for循环。一个iterable是一个实现迭代器协议的容器,可以一个接一个地生成值(例如:数组,字符串,生成器等)。

function vowelCount(file) {
  let contents = readFile(file)
  let lines = contents.split('\n') // split content into array of lines
  let result = [] // an array of arrays where each index maps to a line
                  // and each index within the inner array maps to the
                  // vowel count for a word on that line

  for (let line of lines) {
    let temp = []
    let words = line.split(/\s+/)

    for (let word of words) {
      let vowelCount = 0

      for (let char of word) {
        if (
          'a' === char || 'e' === char || 'i' === char || 'o' === char || 'u' === char
        ) vowelCount++
      }
      temp.push(vowelCount)
    }
    result.push(temp)
  }
  return result
}

The solution above is not extendable, not scalable and gives us no reusable components. An alternate approach is to use higher-order functions and composition.上面的解决方案是不可扩展的,不可扩展,并且给我们没有可重用的组件。另一种方法是使用高阶函数和组合。

// main.js
function vowelOccurrences(file) {
  return map(words => map(vowelCount, words), listOfWordsByLine(read(file)))
}

function vowelCount(word) {
  return reduce((prev, char) => {
    if (
      'a' === char || 'e' === char || 'i' === char || 'o' === char || 'u' === char
    ) return ++prev
    else return prev
  }, 0, word)
}

function listOfWordsByLine(string) {
  return map(line => split(/\s+/, line), split('\n', string))
}
// reusable utils in util.js
function reduce(fn, accumulator, list) {
  return [].reduce.call(list, fn, accumulator)
}

function map(fn, list) {
  return [].map.call(list, fn)
}

function split(splitOn, string) {
  return string.split(splitOn)
}

listOfWordsByLine returns an array of arrays where each element corresponds to an array of words that make up a line. For example:listOfWordsByLine返回一个数组数组,其中每个元素对应于构成一行的单词数组。例如:

let input = 'line one\nline two'
listOfWordsByLine(input) // [['line','one'],['line','two']]

In the code above, vowelCount counts the number of vowels in a word. vowelOccurrencesuses vowelCount on the output of listOfWordsByLine to calculate the vowel count per line per word.

The second approach results in a few reusable functions that we can employ throughout our codebase and compose together to solve bigger problems. Thus higher-order functions and composition promote a bottom-up approach which can lead to succint and modular code.

在上面的代码中,vowelCount计算一个单词中元音的数量。 vowelOccurrences在listOfWordsByLine的输出上使用vowelCount来计算每个单词每行的元音数。
第二种方法产生了一些可重用的功能,我们可以在整个代码库中使用,并组合起来解决更大的问题。因此,高阶函数和组合可以促成自下而上的方法,从而导致succint和模块化代码。

Lazy evaluation惰性求值

So what is lazy evaluation?

Lazy evaluation is a strategy where the evaluation of a piece of code is deferred until its results are needed.

In this article I’m going to focus on lazily consuming data and building lazy pipelines that have to be manually drained. I am not going to talk about how lazy evaluation is implemented at a language level (no graph reduction, normal form, etc.).

Let’s look at an example. Given a list of integers, square the elements of this list, and print the sum of the first four squared elements. To write a lazy implementation for this, we must first figure out when do we need to compute something. In our case, only when we want to sum the first four squared elements do we need to provide the squared elements. Therefore we can defer the squaring operation until we actually start summing. Armed with this knowledge, let’s implement a lazy solution.

那么什么是懒惰的评价?
懒惰评估是一种策略,其中一段代码的评估推迟到需要结果为止。
在这篇文章中,我将着重于懒惰地消耗数据,并建立必须手动排除的惰性管道。我不会谈论在语言层面上执行懒惰评估(没有图表缩小,正常表单等)。
我们来看一个例子。给定整数列表,对该列表的元素进行平方,并打印前四个方形元素的总和。为了写一个懒惰的实现,我们必须首先弄清楚我们什么时候需要计算一些东西。在我们的情况下,只有当我们想要总和前四个方阵元素时,我们需要提供平方的元素。因此,我们可以推迟平方运算,直到我们实际开始求和。掌握这些知识,让我们实现一个懒惰的解决方案。

let squareAndSum = (iterator, n) => {
  let result = 0

  while(n > 0) {
    try {
      result += Math.pow(iterator.next(), 2)
      n--
    }
    catch(_) {
      // length of list was lesser than `n` hence
      // iterator.next threw to signal it's out of values
      break
    }
  }
  return result
}

let getIterator = (arr) => {
  let i = 0

  return {
    next: function() {
      if (i < arr.length) return arr[i++]
      else throw new Error('Iteration done')
    }
  }
}

let squareAndSumFirst4 = (arr) => {
  let iterator = getIterator(arr)

  return squareAndSum(iterator, 4)
}

In the implementation, we start squaring elements only when the summing starts. Therefore, only those elements that are being summed are squared. This is achieved by controlling iteration and how values are yielded. A custom iteration protocol is implemented that yields elements one by one and signals when we have no more elements to yield. It is quite similar to what a lot of languages use. This protocol is encapsulated in an iterator object. The iterator object contains one function, next, which takes zero parameters. It yields the next element if there is one and throws otherwise.

The squareAndSum function takes as input an iterator object and n, the number of elements to sum. It pulls n values out of the iterator (by calling .next() n-times), squares and then sums them. getIterator gives us an iterator that wraps our list (which we call an iterable since we can iterate over it). squareAndSumFirst4 then uses getIteratorand squareAndSum to give us the sum of first four numbers of the input list squared lazily. A nice side effect of using iterators is that it enables us to implement data structures that can yield infinite values.

Having to implement all of the above each time we require an iterator can be painful. Luckily, ES6 has introduced a simple way of writing iterators. They are called generators.

A generator is a pausable function that can yield values to its caller using the yieldkeyword multiple times during its execution. Generators, when invoked, return a Generator object. We can call next on the Generator object to get the next value. In JavaScript we create generators by defining a function with a *. Here’s an example.

在实现中,只有当求和开始时,才开始平均元素。因此,只有那些被求和的元素是平方的。这是通过控制迭代和如何产生价值来实现的。实现了一种自定义迭代协议,当我们没有更多的元素可以产生时,它会逐个生成元素。它与很多语言使用非常相似。该协议封装在迭代器对象中。迭代器对象包含一个函数,接下来,它接收零个参数。它产生下一个元素,如果有一个,否则抛出。
squareAndSum函数将迭代器对象作为输入,n,要求的元素数。它将n个值从迭代器中拉出(通过调用.next()n次),然后将它们相加。 getIterator给我们一个包装我们的列表的迭代器(我们称之为迭代,因为我们可以迭代它)。 squareAndSumFirst4然后使用getIterator和squareAndSum给我们懒惰的输入列表的前四个数字的和。使用迭代器的一个很好的副作用是它使我们能够实现可以产生无限值的数据结构。
每次我们需要迭代器时,必须执行上述所有操作可能会很痛苦。幸运的是,ES6引入了一种简单的写迭代器的方法。它们被称为发电机。
生成器是一个可暂停的函数,可以在执行期间使用yield关键字多次来为其调用者产生值。生成器在调用时返回Generator对象。我们可以在Generator对象上调用next来获取下一个值。在JavaScript中,我们通过使用*定义一个函数来创建生成器。这是一个例子。

// a generator that returns an infinite list of sequential返回一个无限列的顺序的生成器
// integers starting from 0从0开始的整数
// notice the "*" to tell the parser that this is a generator注意“*”来告诉解析器这是一个生成器
function* numbers() {
  let i = 0

  yield 'starting infinite list of numbers'
  while (true) yield i++
}

let n = numbers() // get an iterator from the generator
n.next()          // {value: "starting infinite list of numbers", done: false}
n.next()          // {value: 0, done: false}
n.next()          // {value: 1, done: false}
// and so on..

A Generator object conforms to both the iterator and the iterable protocol. Hence it can be used with for-of to access the values it yields. Generator对象符合迭代器和迭代协议。因此,它可以与for-of一起使用来获取它产生的值。

for (let n of numbers()) console.log(n) // will print infinite list of numbers

Now that we know a bit about lazy evaluation, higher-order functions, and function composition, let’s implement something to see how using these three approaches cleans up our code. 现在我们知道一些关于懒惰评估,高阶函数和函数组合的方法,让我们来看看如何使用这三种方法来清理代码。

The problem问题

We are given a file that contains a username on each line. The file may potentially be larger than the RAM available. We are given a function that reads the next chunk from disk and gives us a chunk that ends with a newline. We are to get the usernames that start with “A” or “E” or “M.” We are then supposed to make requests with the usernames to http://jsonplaceholder.typicode.com/users?username=<username>. After this, we are to run a given set of four functions on the query response for the first four requests.

Sample file contents:

我们给了一个包含每一行用户名的文件。该文件可能会比可用的RAM大。我们有一个功能,从磁盘读取下一个块,并给我们一个以换行符结尾的块。我们要获取以“A”或“E”或“M.”开头的用户名。然后,我们应该以http://jsonplaceholder.typicode.com/users?username= <username>的用户名进行请求。之后,我们将对前四个请求的查询响应运行一组给定的四个函数。
示例文件内容:

Bret
Antonette
Samantha
Karianne
Kamren
Leopoldo_Corkery
Elwyn.Skiles
Maxime_Nienow
Delphine
Moriah.Stanton

Let’s break up the problem into smaller chunks that we can write separate functions for. One approach would be to use the following functions:

  • one that returns each username (getNextUsername)
  • one to filter out usernames that begin with an “A”, “E” or “M” (filterIfStartsWithAEOrM)
  • one that makes network requests and returns a promise (makeRequest)

让我们将问题分解成更小的块,我们可以为其分别编写函数。一种方法是使用以下功能:
一个返回每个用户名(getNextUsername)
一个用于过滤掉以“A”,“E”或“M”开头的用户名(filterIfStartsWithAEOrM)

一个使网络请求返回承诺(makeRequest)

The functions above operate on values. We need a way of applying these functions to a list of values. We need higher-order functions that do the following:

  • one that filters items from a list based on a predicate (filter)
  • one that applies a function to every item in a list (map)
  • one that applies functions from one iterable to data from another iterable (zipWithwith a zipping function)

上述功能以值为准。我们需要一种将这些函数应用于值列表的方法。我们需要执行以下操作的高阶函数:
一个基于谓词(过滤器)从列表中筛选项目
一个将功能应用于列表中的每个项目(地图)
一个将功能从一个可迭代应用到来自另一个可迭代的数据(zip带有拉丁函数)
This whole approach can benefit by being lazy so that we dont make network requests for all of the usernames that match our criteria, but only for the first n where n is the number of functions that we have to run on the query responses.

这种整个方法可以通过懒惰来获益,这样我们就不会为符合我们的标准的所有用户名进行网络请求,而只针对第一个n,其中n是我们必须在查询响应上运行的函数数。

We are given an array of functions that are to be run on the final responses and a function that gives us the next chunk lazily. We need a function that gives us usernames one by one lazily. To preserve laziness by controlling when values are yielded, let’s build our solutions using generators.

我们被赋予了一系列函数,这些函数将在最终响应中运行,并且这个功能可以让我们懒洋洋地下一个块。我们需要一个让我们的用户名逐个放松的功能。通过控制何时产生值来保持懒惰,让我们用生成器构建我们的解决方案。

// functions that are run on the query response 在查询响应中运行的函数
let fnsToRunOnResponse = [f1, f2, f3, f4]

// mocks yielding the next chunk of data read from file
// the * denotes that this function is a generator in JavaScript
function* getNextChunk() {
  yield 'Bret\nAntonette\nSamantha\nKarianne\nKamren\nLeopoldo_Corkery\nElwyn.Skiles\nMaxime_Nienow\nDelphine\nMoriah.Stanton\n'
}

// getNextUsername takes an iterator that yields the next chunk ending with a newline
// It itself returns an iterator that yields the usernames one at a time
// getNextUsername接受一个迭代器,产生以换行符结尾的下一个块
//它本身返回一个迭代器,它一次产生一个用户名
function* getNextUsername(getNextChunk) {
  for (let chunk of getNextChunk()) {
    let lines = chunk.split('\n')

    for (let l of lines) if (l !== '') yield l
  }
}

Before writing the next bit of our solution, let’s have a look at ES6 Promises. A Promise is a placeholder for a future value of an incomplete operation. The ES6 Promise interface lets us define what should execute once the operation completes or fails. If the operation is successful, it invokes the success handler with the value of the operation. Otherwise, it invokes the failure handler with the error.

Coming back to our solution, let’s write the functions that operate on values. We need a function that returns true if a value meets our filter criteria and false otherwise. We also need a function that returns a URL when given a username. Lastly, we need a function that, when given a URL, makes a request and returns a promise for that request.

在写下我们的解决方案之前,让我们来看看ES6 Promises。承诺是未来价值不完整的占位符。 ES6 Promise界面允许我们定义操作完成或失败后应执行的操作。如果操作成功,它将使用操作的值调用成功处理程序。否则,它会使用错误调用失败处理程序。
回到我们的解决方案,让我们来编写对值进行操作的函数。如果值满足我们的过滤条件,我们需要一个返回true的函数,否则返回false。我们还需要一个在给定用户名时返回URL的函数。最后,我们需要一个函数,当给定URL时,发出请求并返回该请求的承诺。

// this function returns true if the username meets our criteria
// and false otherwise
let filterIfStartsWithAEOrM = username => {
  let firstChar = username[0]

  return 'A' === firstChar || 'E' === firstChar || 'M' === firstChar
}

// makeRequest makes an ajax request to the URL and returns a promise
// it uses the new fetch api and fat arrows from ES6
// it's a normal function and not a generator
let makeRequest = url => fetch(url).then(response => response.json())

// makeUrl takes a username and generates a URL that we want to query
let makeUrl = username => 'http://jsonplaceholder.typicode.com/users?username=' + username

Now that we have functions that operate on values, we need functions that can apply these values to lazy lists of data. These are going to be higher-order functions. They should be lazy and should defer execution until they are explicitly asked to execute. This sounds like a good place to use generators, since we need values on demand.

现在我们具有对值进行操作的函数,我们需要可以将这些值应用于延迟数据列表的函数。这些将是更高阶的功能。他们应该是懒惰的,应该推迟执行,直到被明确要求执行。这听起来像是使用发电机的好地方,因为我们需要价值。

// filter accepts a function (the predicate) that takes a value and returns a
// boolean and an iterator filter itself returns an that iterator yields the
// value iff the function when applied to the value returns true
function* filter(p, a) {
  for (let x of a)
    if (p(x)) yield x
}


// map takes a function and an iterator
// it returns a new iterator that yields the result of applying the function to each value
// in the iterator that was given to it originally
function* map(f, a) {
  for (let x of a) yield f(x)
}

// zipWith takes a binary function and two iterators as input
// it returns an iterator which in turn applies the given function to values from each of
// iterators and yields the result.
function* zipWith(f, a, b) {
  let aIterator = a[Symbol.iterator]()
  let bIterator = b[Symbol.iterator]()
  let aObj, bObj

  while (true) {
    aObj = aIterator.next()
    if (aObj.done) break
    bObj = bIterator.next()
    if (bObj.done) break
    yield f(aObj.value, bObj.value)
  }
}

// execute makes a deferred iterator begin execution
// it basically calls `.next` on the iterator repeatedly
// till the iterator is done
function execute(iterator) {
  for (x of iterator) ;; // drain the iterator
}

So now that we have the functions that we would need, let’s compose them to build our solution.

所以现在我们有我们需要的功能,我们来组合它来构建我们的解决方案。

let filteredUsernames        = filter(filterIfStartsWithAEOrM, getNextUsername(getNextChunk)

let urls                     = map(makeUrl, filteredUsernames)

let requestPromises          = map(makeRequest, urls)

let applyFnToPromiseResponse = (fn, promise) => promise.then(response => fn(response))

let lazyComposedResult       = zipWith(applyFnToPromiseResponse, fnsToRunOnResponse, requestPromises)

execute(lazyComposedResult)

lazyComposedResult is a lazy pipeline of composed function applications. No step in our pipeline will execute until we call execute on the final composed piece i.e., lazyComposedResult to start the process. Therefore, we will make only four network calls even though our result set post filtering might contain more than four values.

As a result we now have reusable functions that operate on values, reusable higher-order functions, and a way to compose all these together to write a succinct solution.

lazyComposedResult是一个组合函数应用程序的懒惰管道。 我们的流水线中的任何步骤都将执行,直到我们调用执行在最后的组合片段,即lazyComposedResult来启动进程。 因此,即使我们的结果集后置过滤可能包含四个以上的值,我们也只能进行四次网络呼叫。
因此,我们现在具有可重用的功能,可以使用值,可重用的高阶函数,以及组合所有这些功能以编写简洁的解决方案。

Epilogue结语

In this article, we defined higher-order functions, function composition and lazy evaluation. We then went through examples of each. Finally, we combined all the three approaches to write a lazy, modular and composable solution to a given problem.

在本文中,我们定义了高阶函数,函数组合和懒惰评估。 然后,我们通过了每个例子。 最后,我们将所有三种方法结合起来,将一个懒惰,模块化和可组合的解决方案写入给定的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值