Comment synchroniser correctement une fonction en JavaScript?

J'ai écrit une fonction curry simple en JavaScript qui fonctionne correctement pour la plupart des cas:

 var add = curry(function (a, b, c) { return a + b + c; }); var add2 = add(2); var add5 = add2(3); alert(add5(5)); 
 <script> function curry(f) { var length = f.length; if (length > 0) { return partial(f, length, []); } else { return f; // f is already curried } } function partial(f, length, a) { return function () { var arity = length; var count = arguments.length; var args = new Array(count); var index = 0; while (index < count) { args[index] = arguments[index++]; } args = a.concat(args); return count < arity ? partial(f, arity - count, args) : f.apply(this, args); }; } </script> 

Toutefois, cela ne fonctionne pas pour le cas suivant:

 // length :: [a] -> Number function length(a) { return a.length; } // filter :: (a -> Bool) -> [a] -> [a] var filter = curry(function (f, a) { return a.filter(f); }); // compose :: (b -> c) -> (a -> b) -> a -> c var compose = curry(function (f, g, x) { return f(g(x)); }); // countWhere :: (a -> Bool) -> [a] -> Number var countWhere = compose(compose(length), filter); 

Selon le nombre de questions suivantes, il est défini comme (length .) . filter (length .) . filter :

Que fait (f.). G signifie chez Haskell?

Par conséquent, je devrais pouvoir utiliser le countWhere suivant:

 countWhere(odd, [1,2,3,4,5]); function odd(n) { return n % 2 === 1; } 

Cependant, au lieu de renvoyer 3 (la longueur du tableau [1,3,5] ), il renvoie une fonction. Qu'est-ce que je fais mal?

@adadit

Je publie ceci parce que vous avez partagé un commentaire sur ma réponse à "combiner" les fonctions de javascript de manière fonctionnelle? Je n'ai pas spécifiquement couvert le curry dans ce post parce que c'est un sujet très controversé et pas vraiment une canette de vers que je voulais ouvrir là-bas.

Je serais méfiant en utilisant le libellé "comment curry correct " lorsque vous semblez ajouter votre propre sucre et vos commodités dans votre mise en œuvre.

Quoi qu'il en soit, je ne pense vraiment pas que ce soit un argument argumentatif / combatif. Je voudrais avoir une discussion ouverte et amicale sur le currying en JavaScript tout en soulignant certaines des différences entre nos approches.

Sans plus tarder…


Clarifier:

Étant donné f est une fonction et f.length est n . Laissez curry(f) être g . Nous appelons g avec m arguments. Que se passerait-il? Vous dites:

  1. Si m === 0 il suffit de retourner g .
  2. Si m < n s'applique partiellement f aux m nouveaux arguments, et renvoie une nouvelle fonction curry qui accepte les arguments n - m restants.
  3. Si m === n appliquez f aux arguments m . Si le résultat est une fonction, utilisez le curry du résultat. Enfin, retournez le résultat.
  4. Si m > n applique f pour les premiers arguments n . Si le résultat est une fonction, utilisez le curry du résultat. Enfin, appliquez le résultat aux arguments m - n restants et renvoyez le nouveau résultat.

Voyons un exemple de code de ce que le code de @Aadit M Shah fait réellement

 var add = curry(function(x, y) { return function(a, b) { return x + y + a + b; } }); var z = add(1, 2, 3); console.log(z(4)); // 10 

Il y a deux choses qui se passent ici:

  1. Vous tentez de prendre en charge les fonctions de curry avec des arguments variadiques.
  2. Vous roulez automatiquement les fonctions retournées

Je ne crois pas qu'il y ait beaucoup de place pour le débat ici, mais les gens semblent manquer ce qui est en train de curry.

Via: Wikipedia
En mathématiques et en informatique, le currying est la technique de traduction de l'évaluation d'une fonction qui nécessite plusieurs arguments (ou un tuple d'arguments) en évaluant une séquence de fonctions, chacune avec un seul argument

J'estime ce dernier bit parce que c'est tellement important; Chaque fonction dans la séquence ne prend qu'un seul argument ; Pas des arguments variadiques (0, 1 ou plus) comme vous le suggérez.

Vous mentionnez haskell dans votre publication, aussi, je suppose que vous savez que Haskell n'a pas de fonctions qui prennent plus d'un argument. (Remarque: une fonction qui prend un tuple est toujours juste une fonction qui prend un argument, un seul tuple). Les raisons de cela sont profondes et vous offrent une souplesse dans l'expressivité qui ne vous est pas offerte par des fonctions avec des arguments variadiques.

Repliquons donc cette question originale: qu'estce qui devrait arriver?

Eh bien, c'est simple lorsque chaque fonction n'accepte que 1 argument. À tout moment, si plus d'un argument est donné, ils sont simplement abandonnés.

 function id(x) { return x; } 

Que se passe-t-il lorsque nous appelons id(1,2,3,4) ? Bien sûr, nous n'obtenons que le 1 retour et 2,3,4 sont totalement ignorés. C'est:

  1. Comment fonctionne JavaScript
  2. Comment Wikipedia dit que le curry devrait fonctionner
  3. Comment nous devrions mettre en œuvre notre propre solution de curry

Avant d'aller plus loin, je vais utiliser les fonctions de flèche ES6 , mais j'ajouterai également l'équivalent ES5 en bas de cette publication. (Probablement plus tard ce soir.)

Technique de curry à la naomik

Dans cette approche, nous écrivons une fonction curry qui renvoie en continu des fonctions à un seul paramètre jusqu'à ce que tous les arguments aient été spécifiés

À la suite de cette implémentation, nous avons 6 fonctions polyvalentes.

 // no nonsense curry const curry = f => { const aux = (n, xs) => n === 0 ? f (...xs) : x => aux (n - 1, [...xs, x]) return aux (f.length, []) } // demo let sum3 = curry(function(x,y,z) { return x + y + z; }); console.log (sum3 (3) (5) (-1)); // 7 

Le problème avec votre fonction de curry (et pour la plupart curry fonctions de curry que les gens écrivent en JavaScript), c'est qu'il ne gère pas correctement les arguments supplémentaires.

Que fait le curry

Supposons que f soit une fonction et f.length soit n . Laissez curry(f) être g . Nous appelons g avec m arguments. Que se passerait-il?

  1. Si m === 0 il suffit de retourner g .
  2. Si m < n s'applique partiellement f aux m nouveaux arguments, et renvoie une nouvelle fonction curry qui accepte les arguments n - m restants.
  3. Sinon, appliquez f aux arguments m et renvoyez le résultat.

C'est ce que font la plupart curry fonctions curry , et c'est faux. Les deux premiers cas ont raison, mais le troisième cas est erroné. Au lieu de cela, il devrait être:

  1. Si m === 0 il suffit de retourner g .
  2. Si m < n s'applique partiellement f aux m nouveaux arguments, et renvoie une nouvelle fonction curry qui accepte les arguments n - m restants.
  3. Si m === n appliquez f aux arguments m . Si le résultat est une fonction, curry le curry du résultat. Enfin, retournez le résultat.
  4. Si m > n applique f pour les premiers arguments n . Si le résultat est une fonction, curry le curry du résultat. Enfin, appliquez le résultat aux arguments m - n restants et renvoyez le nouveau résultat.

Le problème avec la plupart curry fonctions curry

Considérez le code suivant:

 var countWhere = compose(compose(length), filter); countWhere(odd, [1,2,3,4,5]); 

Si nous utilisons les fonctions de curry incorrectes, cela équivaut à:

 compose(compose(length), filter, odd, [1,2,3,4,5]); 

Cependant, compose accepte seulement trois arguments. Le dernier argument est abandonné:

 var compose = curry(function (f, g, x) { return f(g(x)); }); 

Par conséquent, l'expression ci-dessus évalue:

 compose(length)(filter(odd)); 

Ceci s'élève à:

 compose(length, filter(odd)); 

La fonction de compose s'attend à un autre argument, c'est pourquoi il renvoie une fonction au lieu de retourner 3 . Pour obtenir la bonne sortie, vous devez écrire:

 countWhere(odd)([1,2,3,4,5]); 

C'est la raison pour laquelle la plupart curry fonctions curry sont fausses.

La solution utilisant la fonction de curry correcte

Considérons le code suivant à nouveau:

 var countWhere = compose(compose(length), filter); countWhere(odd, [1,2,3,4,5]); 

Si nous utilisons la fonction de curry correcte, cela équivaut à:

 compose(compose(length), filter, odd)([1,2,3,4,5]); 

Qui évalue:

 compose(length)(filter(odd))([1,2,3,4,5]); 

Qui évalue à nouveau (ignorant une étape intermédiaire):

 compose(length, filter(odd), [1,2,3,4,5]); 

Ce qui se traduit par:

 length(filter(odd, [1,2,3,4,5])); 

Produire le bon résultat 3 .

La mise en œuvre de la fonction de curry correcte

Notez que je n'utilise pas slice pour convertir l'objet arguments en un tableau parce que c'est un tueur d'optimisation dans V8:

 function curry(f) { var length = f.length; if (length > 0) return partial(f, length, []); else return f; // f is already curried } function partial(f, length, a) { return function () { var count = arguments.length; var arity = length; var part = count < arity; var size = part ? count : arity; var args = new Array(size); var index = 0; while (index < size) args[index] = arguments[index++]; if (part) return partial(f, arity - count, a.concat(args)); var result = f.apply(null, a.concat(args)); if (typeof result === "function") result = curry(result); if (arity < count) { var args = new Array(count - arity), number = 0; while (index < count) args[number++] = arguments[index++]; return result.apply(null, args); } return result; }; } 

Je ne suis pas sûr de la rapidité avec laquelle cette mise en œuvre du curry est. Peut-être que quelqu'un pourrait le rendre plus rapide.

Implications de l'utilisation de la fonction de curry correcte

L'utilisation de la fonction curry correcte vous permet de traduire directement le code Haskell en JavaScript. Par exemple:

 var id = curry(function (a) { return a; }); var flip = curry(function (f, x, y) { return f(y, x); }); 

La fonction id est utile car elle vous permet d'appliquer partiellement une fonction non-curry facilement:

 var add2 = id(add, 2); function add(a, b) { return a + b; } 

La fonction flip est utile car elle vous permet de créer facilement des sections correctes en JavaScript:

 var sub2 = flip(sub, 2); // equivalent to (x - 2) function sub(a, b) { return a - b; } 

Cela signifie également que vous n'avez pas besoin de hacks comme cette fonction de compose étendue :

Quel est un bon nom pour cette fonction `composée 'étendue?

Vous pouvez simplement écrire:

 var project = compose(map, pick); 

Comme mentionné dans la question, si vous souhaitez composer la length et filter vous utilisez la (f .) . g (f .) . g pattern:

Que fait (f.). G signifie chez Haskell?

Une autre solution est de créer des fonctions de compose supérieure:

 var compose2 = compose(compose, compose); var countWhere = compose2(length, fitler); 

Ceci est tout possible en raison de la mise en œuvre correcte de la fonction curry .

Alimentation supplémentaire pour la pensée

J'utilise habituellement la fonction de chain suivante lorsque je veux composer une chaîne de fonctions:

 var chain = compose(function (a, x) { var length = a.length; while (length > 0) x = a[--length](x); return x; }); 

Cela vous permet d'écrire un code comme:

 var inc = add(1); var foo = chain([map(inc), filter(odd), take(5)]); foo([1,2,3,4,5,6,7,8,9,10]); // [2,4,6] 

Ce qui équivaut au code Haskell suivant:

 let foo = map (+1) . filter odd . take 5 foo [1,2,3,4,5,6,7,8,9,10] 

Il vous permet également d'écrire un code comme:

 chain([map(inc), filter(odd), take(5)], [1,2,3,4,5,6,7,8,9,10]); // [2,4,6] 

Ce qui équivaut au code Haskell suivant:

 map (+1) . filter odd . take 5 $ [1,2,3,4,5,6,7,8,9,10] 

J'espère que cela pourra aider.

Outre sa définition mathématique

Currying est la transformation d'une fonction avec n paramètres en une suite de n fonctions, chacune acceptant un seul paramètre. L'arité est ainsi transformée de n-ary en n * 1-ary

Quel impact a duré la programmation? L'abstraction sur l'arité !

 const comp = f => g => x => f(g(x)); const inc = x => x + 1; const mul = y => x => x * y; const sqr = x => mul(x)(x); comp(sqr)(inc)(1); // 4 comp(mul)(inc)(1)(2); // 4 

comp attend deux fonctions f et g et un arbitraire arbitraire x . Par conséquent, g doit être une fonction unaire (une fonction avec exactement un paramètre formel) et f aussi, puisqu'elle est alimentée avec la valeur de retour de g . Il ne surprendra personne que comp(sqr)(inc)(1) fonctionne. sqr et inc sont tous les deux unaires.

Mais mul est évidemment une fonction binaire. Comment ça va au travail? Parce que le curry extrait l'arity de mul . Vous pouvez maintenant imaginer ce qu'est une caractéristique puissante.

Dans ES2015, nous pouvons pré-curry nos fonctions avec des fonctions de flèche succinctement:

 const map = (f, acc = []) => xs => xs.length > 0 ? map(f, [...acc, f(xs[0])])(xs.slice(1)) : acc; map(x => x + 1)([1,2,3]); // [2,3,4] 

Néanmoins, nous avons besoin d'une fonction de curry programmatique pour toutes les fonctions hors de notre contrôle. Comme nous avons appris que le currying signifie principalement l'abstraction sur l'arité, notre implémentation ne doit pas dépendre de Function.length :

 const curryN = (n, acc = []) => f => x => n > 1 ? curryN(n - 1, [...acc, x])(f) : f(...acc, x); const map = (f, xs) => xs.map(x => f(x)); curryN(2)(map)(x => x + 1)([1,2,3]); // [2,3,4] 

Passer l' curryN explicitement à curryN a l'effet secondaire agréable que nous pouvons curry fonctions variadiques aussi:

 const sum = (...args) => args.reduce((acc, x) => acc + x, 0); curryN(3)(sum)(1)(2)(3); // 6 

Il reste un problème: notre solution de curry ne peut pas traiter les méthodes. OK, nous pouvons redéfinir facilement les méthodes dont nous avons besoin:

 const concat = ys => xs => xs.concat(ys); const append = x => concat([x]); concat([4])([1,2,3]); // [1,2,3,4] append([4])([1,2,3]); // [1,2,3,[4]] 

Une alternative est d'adapter curryN de manière à pouvoir gérer les fonctions et les méthodes à plusieurs arguments.

 const curryN = (n, acc = []) => f => x => n > 1 ? curryN(n - 1, [...acc, x])(f) : typeof f === "function" ? f(...acc, x) : x[f](...acc); curryN(2)("concat")(4)([1,2,3]); // [1,2,3,4] 

Je ne sais pas si c'est la façon correcte de curry fonctions (et les méthodes) dans le Javascript cependant. C'est plutôt une façon possible.

MODIFIER:

Naomik a souligné qu'en utilisant une valeur par défaut, l'API interne de la fonction curry est partiellement exposée. La simplification obtenue de la fonction curry vient donc au détriment de sa stabilité. Pour éviter les fuites d'API, nous avons besoin d'une fonction wrapper. Nous pouvons utiliser le combinateur U (similaire à la solution de naomik avec Y):

 const U = f => f(f); const curryN = U(h => acc => n => f => x => n > 1 ? h(h)([...acc, x])(n-1)(f) : f(...acc, x))([]); 

Inconvénient: la mise en œuvre est plus difficile à lire et a une pénalité de performance.

 //---Currying refers to copying a function but with preset parameters function multiply(a,b){return a*b}; var productOfSixNFiveSix = multiply.bind(this,6,5); console.log(productOfSixNFive()); //The same can be done using apply() and call() var productOfSixNFiveSix = multiply.call(this,6,5); console.log(productOfSixNFive); var productOfSixNFiveSix = multiply.apply(this,[6,5]); console.log(productOfSixNFive);