Introduction aux combinateurs

Les combinateurs sont des fonctions de niveau supérieur qui permettent de combiner des fonctions, des variables ou d'autres combinateurs. En général, ils ne contiennent pas de déclarations de leurs propres variables ou de leur propre logique de gestion. Ils sont les seuls à permettre de rincer le contrôle dans un programme de fonctions.

Dans cet article, j'essaierai d'en couvrir quelques-uns :

Robinet

Un combinateur est très utile pour les fonctions qui ne renvoient rien. Il prend la fonction à laquelle le paramètre est destiné et la renvoie ensuite.

Déclaration

const tap = (fn) => (value) => {
fn(valeur) ;
retourne la valeur ;
} ;

Exemple d'impératif

const [items, setItems] = useState(() => [])

axios
.get('http://localhost')
.then({ data } => {
setItems(data)
console.log(data)
onLoadData(data)
}).then(...) // renvoie undefined - les données de la promesse ont été modifiées

Exemple déclaratif

const [items, setItems] = useState(() => [])

axios
.get('http://localhost')
.then(({ data }) => data)
.then(tap(setItems)) // (data) => { setItems(data) ; return data }
.then(tap(console.log)) // un then = une fonction = une responsabilité
.then(tap(onLoadData))
.then(...) // toujours accès aux données d'origine
// facile de maintenir le principe d'ouverture/fermeture

Curry

Il divise les arguments d'une fonction et permet de les appeler de manière séquentielle.

Déclaration

const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args) ;
} ;

return curried

} ;


Exemple

const curry = (fn) => {
const curried = (...args) => {
if (fn.length !== args.length){
return curried.bind(null, ...args)
}
return fn(...args) ;
} ;

return curried

} ;
const sum = (a, b, c) => a + b + c

const currySum = curry(sum)
/*
appels possibles
currySum(a)(b)(c),
currySum(a)(b,c),
currySum(a,b)(c),
currySum(a,b,c)
*/

currySum(1) // (b, c) => 1 + a + b ou (b) => (c) => 1 + a + b
currySum(5)(10) // (c) => 5 + 10 + b
currySum(5, 10) // (c) => 5 + 10 + b
currySum(5)(10)(20) // 35
currySum(5, 10)(20) // 35
currySum(5)(10, 20) // 35

const divideBy = curry((a, b) => b / a)
const multiplyBy = curry((a, b) => a * b)

const divideByTwo = divideBy(2)
divideByTwo(10) // renvoie 5

const multiplyByFive = multiplyBy(5)
multiplyByFive(10) // renvoie 50

Pipe/Composition

Grâce à la composition, il est possible de passer des données d'une fonction à une autre. Il est important que les fonctions prennent le même nombre d'arguments. La différence entre pipe et compose est que le premier exécute la fonction de la première à la dernière, tandis que compose les appelle à partir de la fin.

Déclaration

const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
valeur) ;

const compose = (...fns) => (value, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valeur) ;
const pipe = (...fns) => (value, ...args) =>
fns.reduce((v, f, i) =>
i === 0
? f(v, ...args)
: f(v),
valeur) ;

const compose = (...fns) => (value, ...args) =>
fns.reduceRight((v, f, i) =>
i === (fns.length - 1)
? f(v, ...args)
: f(v),
valeur) ;

Exemple d'impératif

const sine = (val) => Math.sin(val * Math.PI / 180) // non lisible
 sine(90) // renvoie 1

Exemple déclaratif

const sine = pipe(
multiplyBy(Math.PI) // ↓ val * Math.PI
divideBy(180), // ↓ val * Math.PI / 180
Math.sin, // ↓ Math.sin(val * Math.PI / 180)
)

const sine = compose(
Math.sin, // ↑ Math.sin(val * Math.PI / 180)
divideBy(180), // ↑ val * Math.PI / 180
multiplyBy(Math.PI) // ↑ val * Math.PI
)

sine(90) // renvoie 1

Fourchette

Le combinateur de fourche est utile dans les situations où vous souhaitez traiter une valeur de deux manières différentes et combiner les résultats.

Déclaration

const fork = (join, fn1, fn2) => (value) => join(fn1(value), fn2(value)) ;

Exemple

const length = (array) => array.length
const sum = (array) => array.reduce((a, b) => a + b, 0)
const divide = (a, b) => a / b

const arithmeticAverage = fork(divide, sum, length)

arithmeticAverage([5, 3, 2, 8, 4, 2]) // renvoie 4

Alternance

Ce combinateur prend deux fonctions et renvoie le résultat de la première si elle est vraie. Sinon, il renvoie le résultat de la seconde fonction.

Déclaration

const alt = (fn, orFn) => (valeur) => fn(valeur) || orFn(valeur)

Exemple

const users = [{
uuid: '123e4567-e89b-12d3-a456-426655440000',
name : 'William'
}]

const findUser = ({ uuid : searchesUuid }) =>
users.find(({ uuid }) => uuid === searchesUuid)

const newUser = data => ({ ...data, uuid : uuid() // créer un nouvel uuid })

const findOrCreate = alt(findUser, newUser)

findOrCreate({ uuid : '123e4567-e89b-12d3-a456-426655440000' }) // renvoie l'objet William
findOrCreate({ name : 'John' }) // renvoie l'objet John avec un nouvel uuid

Séquence

Il accepte de nombreuses fonctions indépendantes et transmet le même paramètre à chacune d'entre elles. En général, ces fonctions ne renvoient aucune valeur.

Déclaration

const seq = (...fns) => (val) => fns.forEach(fn => fn(val))

Exemple

const appendUser = (id) => ({ name }) => {
document.getElementById(id).innerHTML = name
}

const printUserContact = pipe(
findOrCreate, // renvoie l'utilisateur
seq(
appendUser('#contact'), // utilisateur => void
console.log, // utilisateur => void
onContactUpdate // utilisateur => void
)
)

printUserContact(userData)
bannière de coopération
fr_FRFrench