Back to Stories

Nicola Capovilla - 3 Agosto, 2020

Pipe function in Javascript

Indice

Cos'è una Pipe Function?

Il concetto di pipe può essere interpretato diversamente a seconda del contesto di utilizzo.
Prendiamo, però, come riferimento la definizione che da Linux nell'articolo: Pipes: A Brief Introduction.

A pipe is a form of redirection that is used in Linux and other Unix-like operating systems to send the output of one program to another program for further processing.

E ancora:

Pipes are used to create what can be visualized as a pipeline of commands, which is a temporary direct connection between two or more simple programs. This connection makes possible the performance of some highly specialized task that none of the constituent programs could perform by themselves. A command is merely an instruction provided by a user telling a computer to do something, such as launch a program. The command line programs that do the further processing are referred to as filters.

Possiamo quindi definire una pipe function come:

Una pipe function è una funzione che accetta una serie di funzioni, le quali, processano un parametro in ingresso e ritornano un output che sarà l'input per la funzione successiva.

Programmazione funzionale

Come si può intuire dalle definizioni riportate precedentemente, il concetto di pipe function va a braccetto con la programmazione funzionale: ovvero il paradigma di programmazione basato sull'apply e il composing di funzioni.

L'idea sintattica fondamentale è quella di un'espressione e le funzioni si definiscono a partire dalle espressioni, usando le variabili come parametro.

Ci si aspetta quindi la composizione di funzioni pure, il cui risultato dipende solo dai parametri in ingresso e non da variabili globali o altri riferimenti esterni.

In Javascript esistono diverse funzioni per la programmazione funzionale, tra cui reduce, che vedremo in seguito.

Implementazione di una pipe function

Sappiamo che una pipe function accetta n funzioni e ogni funzione eseguirà delle operazioni in base al risultato della funzione precedente.
Partiamo quindi da un semplice esercizio e vediamo come evolvere la nostra pipe function:

Dato un numero in ingresso prima sommare 2 e poi moltiplicarlo per 2.

L'espressione risultante è quindi:

(n + 2) * 2

Seguendo il paradigma di programmazione funzionale avremmo quindi due funzioni atomiche:

// Sum 2 to n
const sumTwo = n => {
    return n + 2;
}

// Multiply 2 to n
const multiplyTwo = n => {
    return n * 2;
}
  • sumTwo: prende in ingresso un numero e ritorna la somma del numero con 2;
  • multiplyTwo: prende in ingresso un numero e ritorna la moltiplicazione del numero per 2;

Per ottenere il risultato dell'espressione:

console.log(multiplyTwo(sumTwo(1))); // 6

Generalizziamo il tutto, incapsulandolo in una pipe funzione:

const pipe = (funA, funB) => (arg) => funB(funA(arg));
const result = pipe(sumTwo, multiplyTwo)(1);
console.log(result); // 6

Reduce: refactor della pipe function

L'esempio appena citato però è limitato solamente a pipe function di 2 funzioni, mentre, come spiegato in precedenza, l'obbiettivo è di accettare N funzioni. Seguendo la pipe function realizzata nel capitolo precedente, l'implementazione risultatante sarebbe qualcosa del tipo:

const pipe = (funA, funB, funC, ... , funN) => (arg) => {
  funN( ... funC(funB(funA(arg)))); 
}

A questo punto ci viene in aiuto il metodo Reduce citato in precendeza, che ci permette di ridurre un array in un unico risultato, eseguendo una funzione ad ogni elemento.

Otteniamo quindi la nostra Pipe Function:

const _reduced = (f, g) => (arg) => g(f(arg));
const pipe = (...fns) => fns.reduce(_reduced);

// Example
const res = pipe(
  sumTwo,
  multiplyTwo,
  moduleByTwo,
)(1)

console.log(res); // 0

Spiegazione del reduce

La funzione reduce accetta quattro argomenti:

  • Accumulatore (acc)
  • Valore corrente (cur)
  • Indice corrente (idx)
  • Array sul quale viene eseguito il metodo (src)

Il valore corrente è l'elemento nell'indice corrente mentre l'accumulatore è il risultato degli elementi precedenti (il valore iniziale sarà pari all'elemento nell'indice 0).
Di conseguenza, ciclando il reduce dell'esempio mostrato in precedenza otteniamo:

# Indice 0
_reduced = sumTwo(arg)

# Indice 1
_reduced = (_reduced, multiplyTwo) => multiplyTwo(_reduced) -> multiplyTwo(sumTwo(arg))

# Indice 2
_reduced = (_reduced, moduleByTwo) => moduleByTwo(_reduced) -> moduleByTwo(multiplyTwo(sumTwo(arg)))

res = moduleByTwo(multiplyTwo(sumTwo(arg)))(1)

Validation pipeline: pipe function senza il passaggio dei parametri

In alcune occasioni, potrebbe esserci la necessità di eseguire una serie di funzioni senza preoccuparci del risultato della funzione precedente. Un use case potrebbe essere la validazione di un elemento.
Un elemento deve quindi passare una serie di funzioni che ne verificano la validità (in caso contrario sollevano un'eccezione). Non sapendo se queste funzioni modificano l'elemento passato potrebbe essere utile usare sempre il valore inserito inizialmente, di conseguenza modificheremo il reduced in questo modo:

const validationPipe = (...fns) => (...args) => fns.reduce((res, func) => func(...args), ...args);

// Example
try {
  pipe(
    isNumber,
    isGreaterThan10,
    isAMultipleOf2,
  )(12)

  // Valid

} catch (e) {
  // Invalid
}

Next Step: L'operatore pipeline "|>" (sperimentale)

Recentemente è stato introdotto in Javascript l'operatore pipeline (|>), attualmente in fase sperimentale.
Come da definizione di MDN:

The experimental pipeline operator |> (currently at stage 1) pipes the value of an expression into a function.

Questo operatore permette di poter scrivere:

let url = decodeURI("%21");

In:

let url = "%21" |> decodeURI;

Traducendo l'esempio mostrato in precedenza possiamo ottenere:

// Nested function
const res = moduleByTwo(multiplyTwo(sumTwo(arg)))(1);

// Pipe function
const res = pipe(
  sumTwo,
  multiplyTwo,
  moduleByTwo,
)(1);

// Pipeline operator
const res = 1 |> sumTwo |> multiplyTwo |> moduleByTwo;

Riferimenti

  • http://www.linfo.org/pipes.html
  • https://www.python.it/doc/articoli/funct.html
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Pipeline_operator

Conclusioni

Queste sono alcune possibili soluzioni per sviluppare la propria pipe function e seguire i paradigmi della programmazione funzionale.

Il codice della soluzione proposta è disponibile su Github.

Se vi è un piaciuto l'articolo, vi invito a condividerlo.

Il vostro supporto e feedback significa molto per noi.