欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Let’s experiment with functional generators and the pipeline operator in JavaScript

最编程 2024-04-29 13:34:06
...

Let’s experiment with functional generators and the pipeline operator in JavaScript

Cristi SalcescuBlockedUnblockFollowFollowing
Photo by Patrick Hendry on Unsplash
A generator is a function that returns the next value from the sequence each time it is called.

Let’s start with a simple functional generator that gives the next integer each time is called. It starts from 0.

function sequence() {  let count = 0;  return function() {    const result = count;    count += 1;    return result;  }}
const nextNumber = sequence();nextNumber(); //0nextNumber(); //1nextNumber(); //2

nextNumber() is an infinite generator. nextNumber() is also a closure function.

Finite generator

Generators can be finite. Check the next example where sequence() creates a generator that returns consecutive numbers from a specific interval. At the end of the sequence it returns undefined:

function sequence(from, to){ let count = from; return function(){   if(count< to){      const result = count;      count += 1;      return result;    }  }}
const nextNumber = sequence(10, 15);nextNumber(); //10nextNumber(); //12nextNumber(); //13nextNumber(); //14nextNumber(); //undefined

toList()

When working with generators, we may want at some point to create a list with all the values from the sequence. For this situation, we need a new function toList() that takes a generator and returns all the values from the sequence as an array. The sequence should be finite.

function toList(sequence) {  const arr = [];  let value = sequence();  while (value !== undefined) {    arr.push(value);    value = sequence();  }  return arr;}

Let’s use it with the previous generator.

const numbers = toList(sequence(10, 15));//[10,11,12,13,14]

The pipeline operator

The pipeline operator |> enables us to write data transformations in a more expressive way. The pipeline operator provides syntactic sugar over function calls with a single argument. Consider the next code:

const shortenText = shortenText(capitalize("This is a long text"));
function capitalize(text) {  return text.charAt(0).toUpperCase() + text.slice(1);}
function shortenText(text) {  return text.substring(0, 8).trim();}

With the pipeline operator the transformation can be written like this:

const shortenText = "This is a long text"   |> capitalize   |> shortenText;  //This is

At this moment the pipeline operator is experimental. You can try it using Babel:

  • in package.json file add the babel pipeline plugin:
{  "dependencies": {    "@babel/plugin-syntax-pipeline-operator": "7.2.0"
}}
  • in the .babelrc configuration file add:
{  "plugins": [["@babel/plugin-proposal-pipeline-operator", {             "proposal": "minimal" }]]}

Generators over collections

In Make your code easier to read with Functional Programming I had an example of processing a list of todos . Here is the code:

function isPriorityTodo(task) {  return task.type === "RE" && !task.completed;}
function toTodoView(task) {  return Object.freeze({ id: task.id, desc: task.desc });}
const filteredTodos = todos.filter(isPriorityTodo).map(toTodoView);

In this example, the todos list goes through two transformations. First a filtered list is created, then a second list with the mapped values is created.

With generators, we can do the two transformations and create only one list. For this, we need a generator sequence() that gives the next value from a collection.

function sequence(list) {  let index = 0;  return function() {    if (index < list.length) {      const result = list[index];      index += 1;      return result;    }  };}

filter() and map()

Next, we need two decorators filter() and map(), that work with functional generators.

filter() takes a generator and creates a new generator that only returns the values from the sequence that satisfies the predicate function.

map() takes a generator and creates a new generator that returns the mapped value.

Here are the implementations:

function filter(predicate) {  return function(sequence) {    return function filteredSequence() {      const value = sequence();      if (value !== undefined) {        if (predicate(value)) {          return value;        } else {          return filteredSequence();        }      }    };  };}
function map(mapping) {  return function(sequence) {    return function() {      const value = sequence();      if (value !== undefined) {        return mapping(value);      }    };  };}

I would like to use these decorators with the pipeline operator. So, instead of creating filter(sequence, predicate){ } with two parameters, I created a curried version of it, that will be used like this: filter(predicate)(sequence). This way, it works nicely with the pipeline operator.

Now that we have the toolbox, made of sequence, filter, map and toList functions, for working with generators over collections, we can put all of them in a module ("./sequence"). See below for how to rewrite the previous code using this toolbox and the pipeline operator:

import { sequence, filter, map, take, toList } from "./sequence";
const filteredTodos =  sequence(todos)   |> filter(isPriorityTodo)   |> map(toTodoView)   |> toList;

reduce()

Let’s take another example that computes the price of fruits from a shopping list.

function addPrice(totalPrice, line){   return totalPrice + (line.units * line.price);}
function areFruits(line){   return line.type === "FRT";}
let fruitsPrice = shoppingList.filter(areFruits).reduce(addPrice,0);

As you can see, it requires us to create a filtered list first and then it computes the total on that list. Let’s rewrite the computation with functional generators and avoid the creation of the filtered list.

We need a new function in the toolbox: reduce(). It takes a generator and reduces the sequence to a single value.

function reduce(accumulator, startValue) {  return function(sequence) {    let result = startValue;    let value = sequence();    while (value !== undefined) {      result = accumulator(result, value);      value = sequence();    }    return result;  };}

reduce() has immediate execution.

Here is the code rewritten with generators:

import { sequence, filter, reduce } from "./sequence";
const fruitsPrice = sequence(shoppingList)   |> filter(areFruits)   |> reduce(addPrice, 0);

take()

Another common scenario is to take only the first n elements from a sequence. For this case we need a new decorator take(), that receives a generator and creates a new generator that returns only the first n elements from the sequence.

function take(n) {  return function(sequence) {    let count = 0;    return function() {      if (count < n) {        count += 1;        return sequence();      }    };  };}

Again, this is the curried version of take() that should be called like this: take(n)(sequence).

Here is how you can use take() on an infinite sequence of numbers:

import { sequence, toList, filter, take } from "./sequence";
function isEven(n) {  return n % 2 === 0;}
const first3EvenNumbers = sequence()    |> filter(isEven)   |> take(3)   |> toList;  //[0, 2, 4]

Custom generators

We can create any custom generator and use it with the toolbox and the pipeline operator. Let’s create the Fibonacci custom generator:

function fibonacciSequence() {  let a = 0;  let b = 1;  return function() {    const aResult = a;    a = b;    b = aResult + b;    return aResult;  };}
const fibonacci = fibonacciSequence();fibonacci();fibonacci();fibonacci();fibonacci();fibonacci();
const firstNumbers = fibonacciSequence()    |> take(10)   |> toList;  //[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Conclusion

The pipeline operator makes data transformation more expressive.

Functional generators can be created over finite or infinite sequences of values.

With generators we can do list processing without creating intermediary lists at each step.

You can check all the samples on codesandbox. I would like to hear your thoughts on this approach. For more on the JavaScript’s functional side take a look at:

Discover Functional Programming in JavaScript with this thorough introduction

Discover the power of closures in JavaScript

How point-free composition will make you a better functional programmer

How to make your code better with intention-revealing function names

Here are a few function decorators you can write from scratch

Make your code easier to read with Functional Programming