Let’s experiment with functional generators and the pipeline operator in JavaScript
Let’s experiment with functional generators and the pipeline operator in JavaScript
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
上一篇: 聚焦 AUTOSAR FiM,发布汽车诊断新工具!
下一篇: 费舍尔信息。