Combinateur U
En se passant une fonction à elle-même comme argument, une fonction peut se reproduire en utilisant son paramètre au lieu de son nom! Ainsi, la fonction donnée à U
doit avoir au moins un paramètre qui se liera à la fonction (elle-même).
Dans l'exemple ci-dessous, nous n'avons pas de condition de sortie, nous allons donc simplement boucler indéfiniment jusqu'à ce qu'un débordement de pile se produise
const U = f => f (f) // call function f with itself as an argument
U (f => (console.log ('stack overflow imminent!'), U (f)))
Nous pouvons arrêter la récursion infinie en utilisant une variété de techniques. Ici, j'écrirai notre fonction anonyme pour renvoyer une autre fonction anonyme qui attend une entrée; dans ce cas, un certain nombre. Lorsqu'un nombre est fourni, s'il est supérieur à 0, nous continuerons de nous répéter, sinon nous retournerons 0.
const log = x => (console.log (x), x)
const U = f => f (f)
// when our function is applied to itself, we get the inner function back
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
// returns: (x => x > 0 ? U (f) (log (x - 1)) : 0)
// where f is a reference to our outer function
// watch when we apply an argument to this function, eg 5
U (f => x => x > 0 ? U (f) (log (x - 1)) : 0) (5)
// 4 3 2 1 0
Ce qui n'est pas immédiatement apparent ici, c'est que notre fonction, lorsqu'elle est appliquée pour la première fois à elle-même à l'aide du U
combinateur, renvoie une fonction en attente de la première entrée. Si nous avons donné un nom à cela, peut effectivement construire des fonctions récursives en utilisant des lambdas (fonctions anonymes)
const log = x => (console.log (x), x)
const U = f => f (f)
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
Seulement ce n'est pas une récursivité directe - une fonction qui s'appelle elle-même en utilisant son propre nom. Notre définition de countDown
ne se référence pas à l'intérieur de son corps et toujours la récursivité est possible
// direct recursion references itself by name
const loop = (params) => {
if (condition)
return someValue
else
// loop references itself to recur...
return loop (adjustedParams)
}
// U combinator does not need a named reference
// no reference to `countDown` inside countDown's definition
const countDown = U (f => x => x > 0 ? U (f) (log (x - 1)) : 0)
Comment supprimer l'auto-référence d'une fonction existante à l'aide du combinateur U
Ici, je vais vous montrer comment prendre une fonction récursive qui utilise une référence à elle-même et la changer en une fonction qui utilise le combinateur U à la place de l'auto-référence
const factorial = x =>
x === 0 ? 1 : x * factorial (x - 1)
console.log (factorial (5)) // 120
Maintenant, en utilisant le combinateur U pour remplacer la référence interne à factorial
const U = f => f (f)
const factorial = U (f => x =>
x === 0 ? 1 : x * U (f) (x - 1))
console.log (factorial (5)) // 120
Le modèle de remplacement de base est le suivant. Notez mentalement, nous utiliserons une stratégie similaire dans la section suivante
// self reference recursion
const foo = x => ... foo (nextX) ...
// remove self reference with U combinator
const foo = U (f => x => ... U (f) (nextX) ...)
Combinateur Y
en relation: les combinateurs U et Y expliqués en utilisant une analogie de miroir
Dans la section précédente, nous avons vu comment transformer la récursivité d'auto-référence en une fonction récursive qui ne repose pas sur une fonction nommée à l'aide du combinateur U. Il y a un peu de gêne à ne pas oublier de toujours se passer la fonction comme premier argument. Eh bien, le combinateur Y s'appuie sur le combinateur U et supprime ce bit fastidieux. C'est une bonne chose car supprimer / réduire la complexité est la principale raison pour laquelle nous créons des fonctions
Tout d'abord, dérivons notre propre combinateur Y
// standard definition
const Y = f => f (Y (f))
// prevent immediate infinite recursion in applicative order language (JS)
const Y = f => f (x => Y (f) (x))
// remove reference to self using U combinator
const Y = U (h => f => f (x => U (h) (f) (x)))
Nous allons maintenant voir comment son utilisation se compare au combinateur U. Remarquez, pour se reproduire, au lieu de U (f)
nous pouvons simplement appelerf ()
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
Y (f => (console.log ('stack overflow imminent!'), f ()))
Maintenant, je vais vous montrer le countDown
programme en utilisant Y
- vous verrez que les programmes sont presque identiques mais le combinateur Y garde les choses un peu plus propres
const log = x => (console.log (x), x)
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const countDown = Y (f => x => x > 0 ? f (log (x - 1)) : 0)
countDown (5)
// 4 3 2 1 0
countDown (3)
// 2 1 0
Et maintenant nous verrons factorial
aussi
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const factorial = Y (f => x =>
x === 0 ? 1 : x * f (x - 1))
console.log (factorial (5)) // 120
Comme vous pouvez le voir, f
devient le mécanisme de récursivité lui-même. Pour se reproduire, nous l'appelons comme une fonction ordinaire. Nous pouvons l'appeler plusieurs fois avec des arguments différents et le résultat sera toujours correct. Et comme c'est un paramètre de fonction ordinaire, nous pouvons le nommer comme nous voulons, comme recur
ci-dessous -
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (recur => n =>
n < 2 ? n : recur (n - 1) + (n - 2))
console.log (fibonacci (10)) // 55
Combinateur U et Y avec plus d'un paramètre
Dans les exemples ci-dessus, nous avons vu comment nous pouvons boucler et passer un argument pour garder une trace de "l'état" de notre calcul. Mais que se passe-t-il si nous devons suivre l'état supplémentaire?
Nous pourrions utiliser des données composées comme un tableau ou quelque chose comme ça ...
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => ([a, b, x]) =>
x === 0 ? a : f ([b, a + b, x - 1]))
// starting with 0 and 1, generate the 7th number in the sequence
console.log (fibonacci ([0, 1, 7]))
// 0 1 1 2 3 5 8 13
Mais c'est mauvais car cela expose l'état interne (compteurs a
et b
). Ce serait bien si nous pouvions simplement appeler fibonacci (7)
pour obtenir la réponse que nous voulons.
En utilisant ce que nous savons sur les fonctions curry (séquences de fonctions unaires (1 paramètre)), nous pouvons atteindre notre objectif facilement sans avoir à modifier notre définition Y
ou à nous fier aux données composées ou aux fonctionnalités avancées du langage.
Regardez la définition de fibonacci
près ci-dessous. Nous appliquons immédiatement 0
et 1
qui sont liés à a
et b
respectivement. Maintenant fibonacci attend simplement que le dernier argument soit fourni auquel sera lié x
. Lorsque nous récurons, nous devons appeler f (a) (b) (x)
(non f (a,b,x)
) car notre fonction est sous forme curry.
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const fibonacci = Y (f => a => b => x =>
x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
console.log (fibonacci (7))
// 0 1 1 2 3 5 8 13
Ce type de modèle peut être utile pour définir toutes sortes de fonctions. Ci - dessous , nous verrons deux fonctions définies en utilisant le Y
combinateur ( range
et reduce
) et un dérivé de reduce
, map
.
const U = f => f (f)
const Y = U (h => f => f (x => U (h) (f) (x)))
const range = Y (f => acc => min => max =>
min > max ? acc : f ([...acc, min]) (min + 1) (max)) ([])
const reduce = Y (f => g => y => ([x,...xs]) =>
x === undefined ? y : f (g) (g (y) (x)) (xs))
const map = f =>
reduce (ys => x => [...ys, f (x)]) ([])
const add = x => y => x + y
const sq = x => x * x
console.log (range (-2) (2))
// [ -2, -1, 0, 1, 2 ]
console.log (reduce (add) (0) ([1,2,3,4]))
// 10
console.log (map (sq) ([1,2,3,4]))
// [ 1, 4, 9, 16 ]
C'EST TOUT ANONYME OMG
Comme nous travaillons ici avec des fonctions pures, nous pouvons substituer n'importe quelle fonction nommée à sa définition. Regardez ce qui se passe lorsque nous prenons fibonacci et remplaçons les fonctions nommées par leurs expressions
/* const U = f => f (f)
*
* const Y = U (h => f => f (x => U (h) (f) (x)))
*
* const fibonacci = Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1)
*
*/
/*
* given fibonacci (7)
*
* replace fibonacci with its definition
* Y (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*
* replace Y with its definition
* U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
//
* replace U with its definition
* (f => f (f)) U (h => f => f (x => U (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
*/
let result =
(f => f (f)) (h => f => f (x => h (h) (f) (x))) (f => a => b => x => x === 0 ? a : f (b) (a + b) (x - 1)) (0) (1) (7)
console.log (result) // 13
Et là vous l'avez - fibonacci (7)
calculé récursivement en utilisant rien d'autre que des fonctions anonymes