Je viens d'apprendre le curry, et bien que je pense comprendre le concept, je ne vois aucun grand avantage à l'utiliser.
Comme exemple trivial, j'utilise une fonction qui ajoute deux valeurs (écrites en ML). La version sans curry serait
fun add(x, y) = x + y
et serait appelé comme
add(3, 5)
tandis que la version au curry est
fun add x y = x + y
(* short for val add = fn x => fn y=> x + y *)
et serait appelé comme
add 3 5
Il me semble que ce n'est que du sucre syntaxique qui supprime un ensemble de parenthèses pour définir et appeler la fonction. J'ai vu le curry répertorié comme l'une des caractéristiques importantes des langages fonctionnels, et je suis un peu déçu par le moment. Le concept de créer une chaîne de fonctions qui consomment chacune un seul paramètre, au lieu d'une fonction qui prend un Tuple semble assez compliqué à utiliser pour un simple changement de syntaxe.
La syntaxe légèrement plus simple est-elle la seule motivation pour le curry, ou manque-t-il d'autres avantages qui ne sont pas évidents dans mon exemple très simple? Le curry est-il juste du sucre syntaxique?
Avec les fonctions au curry, vous obtenez une réutilisation plus facile de fonctions plus abstraites, car vous vous spécialisez. Disons que vous avez une fonction d'ajout
add x y = x + y
et que vous souhaitez ajouter 2 à chaque membre d'une liste. À Haskell, vous feriez ceci:
map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific
Ici, la syntaxe est plus légère que si vous deviez créer une fonction add2
add2 y = add 2 y
map add2 [1, 2, 3]
ou si vous deviez créer une fonction lambda anonyme:
map (\y -> 2 + y) [1, 2, 3]
Il vous permet également d'abstraire de différentes implémentations. Supposons que vous disposiez de deux fonctions de recherche. L'une à partir d'une liste de paires clé/valeur et une clé à une valeur et une autre à partir d'une carte de clés à valeurs et d'une clé à une valeur, comme ceci:
lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value
Ensuite, vous pouvez créer une fonction qui accepte une fonction de recherche de Clé à Valeur. Vous pouvez lui passer l'une des fonctions de recherche ci-dessus, partiellement appliquée avec une liste ou une carte, respectivement:
myFunc :: (Key -> Value) -> .....
En conclusion: le curry est bon, car il vous permet de spécialiser/d'appliquer partiellement des fonctions à l'aide d'une syntaxe légère, puis de passer ces fonctions partiellement appliquées à des fonctions d'ordre supérieur telles que map
ou filter
. Les fonctions d'ordre supérieur (qui prennent les fonctions comme paramètres ou les produisent comme résultats) sont le pain et le beurre de la programmation fonctionnelle, et les fonctions de curry et partiellement appliquées permettent aux fonctions d'ordre supérieur d'être utilisées de manière beaucoup plus efficace et concise.
La réponse pratique est que le curry facilite la création de fonctions anonymes. Même avec une syntaxe lambda minimale, c'est quelque chose d'une victoire; comparer:
map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]
Si vous avez une syntaxe lambda laide, c'est encore pire. (Je vous regarde, JavaScript, Scheme et Python.)
Cela devient de plus en plus utile à mesure que vous utilisez de plus en plus de fonctions d'ordre supérieur. Bien que j'utilise plus des fonctions d'ordre supérieur dans Haskell que dans d'autres langues, j'ai trouvé que j'utilise en fait la syntaxe lambda moins parce que quelque chose comme les deux tiers du temps, le lambda serait juste une fonction partiellement appliquée. (Et la plupart du temps, je l'extrait dans une fonction nommée.)
Plus fondamentalement, il n'est pas toujours évident de savoir quelle version d'une fonction est "canonique". Par exemple, prenez map
. Le type de map
peut être écrit de deux manières:
map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])
Lequel est le "bon"? C'est en fait difficile à dire. En pratique, la plupart des langues utilisent la première - map prend une fonction et une liste et retourne une liste. Cependant, fondamentalement, ce que fait réellement la carte, c'est faire correspondre les fonctions normales aux fonctions de liste - elle prend une fonction et retourne une fonction. Si la carte est curry, vous n'avez pas à répondre à cette question: elle le fait les deux, d'une manière très élégante.
Cela devient particulièrement important une fois que vous généralisez map
à des types autres que list.
De plus, le curry n'est vraiment pas très compliqué. C'est en fait un peu une simplification par rapport au modèle que la plupart des langues utilisent: vous n'avez besoin d'aucune notion de fonctions d'arguments multiples intégrées dans votre langue. Cela reflète également de plus près le calcul lambda sous-jacent.
Bien sûr, les langages de style ML n'ont pas la notion d'arguments multiples sous forme curry ou non curry. La syntaxe f(a, b, c)
correspond en fait au passage du Tuple (a, b, c)
Dans f
, donc f
ne prend toujours que l'argument. C'est en fait une distinction très utile que j'aurais aimé que d'autres langues aient car cela rend très naturel d'écrire quelque chose comme:
map f [(1,2,3), (4,5,6), (7, 8, 9)]
Vous ne pourriez pas facilement le faire avec des langues qui ont l'idée de plusieurs arguments cuits directement!
Le curry peut être utile si vous avez une fonction que vous transmettez en tant qu'objet de première classe et que vous ne recevez pas tous les paramètres nécessaires pour l'évaluer au même endroit dans le code. Vous pouvez simplement appliquer un ou plusieurs paramètres lorsque vous les obtenez et passer le résultat à un autre morceau de code qui a plus de paramètres et terminer l'évaluation là-bas.
Le code pour accomplir cela sera plus simple que si vous devez d'abord rassembler tous les paramètres.
En outre, il est possible de réutiliser davantage le code, car les fonctions prenant un seul paramètre (une autre fonction curry) ne doivent pas correspondre aussi spécifiquement à tous les paramètres.
(Je vais donner des exemples à Haskell.)
Lorsque vous utilisez des langages fonctionnels, il est très pratique que vous puissiez appliquer partiellement une fonction. Comme dans Haskell, (== x)
Est une fonction qui renvoie True
si son argument est égal à un terme donné x
:
mem :: Eq a => a -> [a] -> Bool
mem x lst = any (== x) lst
sans curry, nous aurions un code un peu moins lisible:
mem x lst = any (\y -> y == x) lst
Ceci est lié à programmation tacite (voir aussi style Pointfree sur le wiki Haskell). Ce style ne se concentre pas sur les valeurs représentées par des variables, mais sur la composition des fonctions et la façon dont l'information circule à travers une chaîne de fonctions. Nous pouvons convertir notre exemple en un formulaire qui n'utilise pas du tout de variables:
mem = any . (==)
Ici, nous voyons ==
Comme une fonction de a
à a -> Bool
Et any
comme une fonction de a -> Bool
À [a] -> Bool
. En les composant simplement, nous obtenons le résultat. Tout cela grâce au curry.
L'inverse, sans curry, est également utile dans certaines situations. Par exemple, disons que nous voulons diviser une liste en deux parties - les éléments inférieurs à 10 et les autres, puis concaténer ces deux listes. Le fractionnement de la liste se fait par partition
(< 10)
(Ici nous utilisons également le curry <
). Le résultat est de type ([Int],[Int])
. Au lieu d'extraire le résultat dans ses première et deuxième parties et de les combiner à l'aide de ++
, Nous pouvons le faire directement en incurvant ++
Comme
uncurry (++) . partition (< 10)
En effet, (uncurry (++) . partition (< 10)) [4,12,11,1]
Est évalué à [4,1,12,11]
.
Il existe également d'importants avantages théoriques:
(a, b) -> c
En a -> (b -> c)
signifie que le résultat de cette dernière fonction est de type b -> c
. En d'autres termes, le résultat est une fonction.La motivation principale (au moins initialement) pour le curry n'était pas pratique mais théorique. En particulier, le currying vous permet d'obtenir efficacement des fonctions multi-arguments sans définir réellement de sémantique pour elles ni définir de sémantique pour les produits. Cela conduit à un langage plus simple avec autant d'expressivité qu'un autre, plus compliqué, et c'est donc souhaitable.
Le curry n'est que du sucre syntaxique, mais vous comprenez légèrement ce que fait le sucre, je pense. Prenant votre exemple,
fun add x y = x + y
est en fait du sucre syntaxique pour
fun add x = fn y => x + y
Autrement dit, (add x) renvoie une fonction qui prend un argument y et ajoute x à y.
fun addTuple (x, y) = x + y
C'est une fonction qui prend un Tuple et ajoute ses éléments. Ces deux fonctions sont en fait assez différentes; ils prennent des arguments différents.
Si vous souhaitez ajouter 2 à tous les numéros d'une liste:
(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]
Le résultat serait [3,4,5]
.
Si vous voulez additionner chaque Tuple dans une liste, en revanche, la fonction addTuple s'intègre parfaitement.
(* Sum each Tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]
(* sum each Tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]
Le résultat serait [12,13,14]
.
Les fonctions au curry sont excellentes lorsqu'une application partielle est utile - par exemple, carte, pli, application, filtre. Considérez cette fonction, qui renvoie le plus grand nombre positif dans la liste fournie, ou 0 s'il n'y a pas de nombre positif:
- val highestPositive = foldr Int.max 0;
val highestPositive = fn : int list -> int
Une autre chose que je n'ai pas encore vue mentionnée est que le curry permet une abstraction (limitée) sur l'arité.
Considérez ces fonctions qui font partie de la bibliothèque de Haskell
(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
Dans chaque cas, la variable de type c
peut être un type de fonction afin que ces fonctions fonctionnent sur un préfixe de la liste des paramètres de leur argument. Sans curry, vous auriez besoin d'une fonctionnalité de langage spéciale pour abstraire sur l'arité des fonctions ou avoir de nombreuses versions différentes de ces fonctions spécialisées pour différentes arités.
Le curry n'est pas seulement du sucre syntaxique!
Considérez les signatures de type de add1
(non durci) et add2
(curry):
add1 : (int * int) -> int
add2 : int -> (int -> int)
(Dans les deux cas, les parenthèses dans la signature de type sont facultatives, mais je les ai incluses pour plus de clarté.)
add1
est une fonction qui prend un 2-Tuple de int
et int
et retourne un int
. add2
est une fonction qui prend un int
et renvoie une autre fonction qui à son tour prend un int
et retourne un int
.
La différence essentielle entre les deux devient plus visible lorsque nous spécifions explicitement l'application de fonction. Définissons une fonction (non curry) qui applique son premier argument à son deuxième argument:
apply(f, b) = f b
Maintenant, nous pouvons voir la différence entre add1
et add2
plus clairement. add1
est appelé avec un 2-Tuple:
apply(add1, (3, 5))
mais add2
est appelé avec un int
puis sa valeur de retour est appelée avec un autre int
:
apply(apply(add2, 3), 5)
EDIT: L'avantage essentiel du curry est que vous obtenez une application partielle gratuite. Disons que vous vouliez une fonction de type int -> int
(disons à map
sur une liste) qui a ajouté 5 à son paramètre. Vous pourriez écrire addFiveToParam x = x+5
, ou vous pourriez faire l'équivalent avec un lambda en ligne, mais vous pourriez aussi beaucoup plus facilement (surtout dans les cas moins triviaux que celui-ci) écrire add2 5
!
Ma compréhension limitée est telle:
1) Application de fonction partielle
L'application de fonction partielle est le processus de retour d'une fonction qui prend moins d'arguments. Si vous fournissez 2 arguments sur 3, il retournera une fonction qui prend 3-2 = 1 argument. Si vous fournissez 1 argument sur 3, il retournera une fonction qui prend 3-1 = 2 arguments. Si vous le vouliez, vous pourriez même appliquer partiellement 3 des 3 arguments et cela retournerait une fonction qui ne prend aucun argument.
Donc, étant donné la fonction suivante:
f(x,y,z) = x + y + z;
Lorsque vous liez 1 à x et que vous l'appliquez partiellement à la fonction ci-dessus f(x,y,z)
, vous obtenez:
f(1,y,z) = f'(y,z);
Où: f'(y,z) = 1 + y + z;
Maintenant, si vous deviez lier y à 2 et z à 3, et appliquer partiellement f'(y,z)
vous obtiendriez:
f'(2,3) = f''();
Où: f''() = 1 + 2 + 3
;
Maintenant, à tout moment, vous pouvez choisir d'évaluer f
, f'
Ou f''
. Je peux donc faire:
print(f''()) // and it would return 6;
ou
print(f'(1,1)) // and it would return 3;
2) Curry
Currying d'autre part est le processus de division d'une fonction en une chaîne imbriquée de fonctions à un argument. Vous ne pouvez jamais fournir plus d'un argument, c'est un ou zéro.
Donc, étant donné la même fonction:
f(x,y,z) = x + y + z;
Si vous le curiez, vous obtiendrez une chaîne de 3 fonctions:
f'(x) -> f''(y) -> f'''(z)
Où:
f'(x) = x + f''(y);
f''(y) = y + f'''(z);
f'''(z) = z;
Maintenant, si vous appelez f'(x)
avec x = 1
:
f'(1) = 1 + f''(y);
Vous obtenez une nouvelle fonction:
g(y) = 1 + f''(y);
Si vous appelez g(y)
avec y = 2
:
g(2) = 1 + 2 + f'''(z);
Vous obtenez une nouvelle fonction:
h(z) = 1 + 2 + f'''(z);
Enfin, si vous appelez h(z)
avec z = 3
:
h(3) = 1 + 2 + 3;
Vous êtes renvoyé 6
.
) Fermeture
Enfin, La fermeture est le processus de capture d'une fonction et de données ensemble comme une seule unité. Une fermeture de fonction peut prendre de 0 à un nombre infini d'arguments, mais elle est également consciente des données qui ne lui sont pas transmises.
Encore une fois, étant donné la même fonction:
f(x,y,z) = x + y + z;
Vous pouvez à la place écrire une clôture:
f(x) = x + f'(y, z);
Où:
f'(y,z) = x + y + z;
f'
Est fermé le x
. Cela signifie que f'
Peut lire la valeur de x à l'intérieur de f
.
Donc, si vous appelez f
avec x = 1
:
f(1) = 1 + f'(y, z);
Vous obtiendriez une fermeture:
closureOfF(y, z) =
var x = 1;
f'(y, z);
Maintenant, si vous avez appelé closureOfF
avec y = 2
Et z = 3
:
closureOfF(2, 3) =
var x = 1;
x + 2 + 3;
Ce qui retournerait 6
Conclusion
Le curry, l'application partielle et les fermetures sont tous quelque peu similaires en ce qu'ils décomposent une fonction en plusieurs parties.
Le curry décompose une fonction de plusieurs arguments en fonctions imbriquées d'arguments uniques qui renvoient des fonctions d'arguments uniques. Il est inutile de curry une fonction d'un ou plusieurs arguments, car cela n'a pas de sens.
Une application partielle décompose une fonction d'arguments multiples en une fonction d'arguments inférieurs dont les arguments maintenant manquants ont été substitués à la valeur fournie.
La fermeture décompose une fonction en une fonction et un ensemble de données où les variables à l'intérieur de la fonction qui n'ont pas été transmises peuvent regarder à l'intérieur de l'ensemble de données pour trouver une valeur à laquelle se lier lorsqu'on lui demande d'évaluer.
Ce qui est déroutant dans tout cela, c'est qu'ils peuvent en quelque sorte être utilisés pour implémenter un sous-ensemble des autres. Donc, en substance, ils sont tous un peu un détail d'implémentation. Ils fournissent tous une valeur similaire en ce que vous n'avez pas besoin de rassembler toutes les valeurs à l'avance et en ce que vous pouvez réutiliser une partie de la fonction, car vous l'avez décomposée en unités discrètes.
Divulgation
Je ne suis en aucun cas un expert du sujet, je n'ai commencé à en apprendre que récemment, et donc je donne ma compréhension actuelle, mais il pourrait y avoir des erreurs que je vous invite à signaler, et je corrigerai comme/si J'en découvre.
Currying (application partielle) vous permet de créer une nouvelle fonction à partir d'une fonction existante en fixant certains paramètres. Il s'agit d'un cas particulier de fermeture lexicale où la fonction anonyme n'est qu'un wrapper trivial qui transmet certains arguments capturés à une autre fonction. Nous pouvons également le faire en utilisant la syntaxe générale pour effectuer des fermetures lexicales, mais une application partielle fournit un sucre syntaxique simplifié.
C'est pourquoi les programmeurs LISP, lorsqu'ils travaillent dans un style fonctionnel, utilisent parfois bibliothèques pour application partielle .
Au lieu de (lambda (x) (+ 3 x))
, qui nous donne une fonction qui ajoute 3 à son argument, vous pouvez écrire quelque chose comme (op + 3)
, et ainsi ajouter 3 à chaque élément d'une certaine liste serait alors (mapcar (op + 3) some-list)
plutôt que (mapcar (lambda (x) (+ 3 x)) some-list)
. Cette macro op
fera de vous une fonction qui prend quelques arguments x y z ...
et invoque (+ a x y z ...)
.
Dans de nombreux langages purement fonctionnels, une application partielle est ancrée dans la syntaxe de sorte qu'il n'y a pas d'opérateur op
. Pour déclencher une application partielle, vous appelez simplement une fonction avec moins d'arguments que nécessaire. Au lieu de produire un "insufficient number of arguments"
erreur, le résultat est fonction des arguments restants.
Pour la fonction
fun add(x, y) = x + y
Il est de la forme f': 'a * 'b -> 'c
Pour évaluer on fera
add(3, 5)
val it = 8 : int
Pour la fonction au curry
fun add x y = x + y
Pour évaluer on fera
add 3
val it = fn : int -> int
Où il s'agit d'un calcul partiel, spécifiquement (3 + y), avec lequel on peut compléter le calcul avec
it 5
val it = 8 : int
ajouter dans le deuxième cas est de la forme f: 'a -> 'b -> 'c
Ce que le curry fait ici, c'est de transformer une fonction qui prend deux accords en un qui ne prend qu'un seul renvoyant un résultat. Évaluation partielle
Pourquoi en aurait-on besoin?
Dire x
sur le RHS n'est pas seulement un entier normal, mais plutôt un calcul complexe qui prend un certain temps à terminer, pour des augmentations, pour l'amour, deux secondes.
x = twoSecondsComputation(z)
Donc, la fonction ressemble maintenant à
fun add (z:int) (y:int) : int =
let
val x = twoSecondsComputation(z)
in
x + y
end;
De type add : int * int -> int
Maintenant, nous voulons calculer cette fonction pour une plage de nombres, mappons-la
val result1 = map (fn x => add (20, x)) [3, 5, 7];
Pour ce qui précède, le résultat de twoSecondsComputation
est évalué à chaque fois. Cela signifie que cela prend 6 secondes pour ce calcul.
L'utilisation d'une combinaison de mise en scène et de curry permet d'éviter cela.
fun add (z:int) : int -> int =
let
val x = twoSecondsComputation(z)
in
(fn y => x + y)
end;
De la forme au curry add : int -> int -> int
Maintenant on peut faire,
val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];
twoSecondsComputation
n'a besoin d'être évalué qu'une seule fois. Pour augmenter l'échelle, remplacez deux secondes par 15 minutes, ou n'importe quelle heure, puis ayez une carte contre 100 nombres.
Résumé: Le currying est excellent lors de l'utilisation avec d'autres méthodes pour des fonctions de niveau supérieur comme outil d'évaluation partielle. Son objectif ne peut pas vraiment être démontré par lui-même.
Le curry permet une composition flexible des fonctions.
J'ai composé une fonction "curry". Dans ce contexte, je me fiche du type d'enregistreur que je reçois ou de son origine. Je me fiche de ce qu'est l'action ou d'où elle vient. Tout ce qui m'importe, c'est de traiter mon entrée.
var builder = curry(function(input, logger, action) {
logger.log("Starting action");
try {
action(input);
logger.log("Success!");
}
catch (err) {
logger.logerror("Boo we failed..", err);
}
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.
La variable de générateur est une fonction qui renvoie une fonction qui renvoie une fonction qui prend mon entrée qui fait mon travail. Il s'agit d'un simple exemple utile et non d'un objet en vue.
Le curry est un avantage lorsque vous n'avez pas tous les arguments d'une fonction. S'il vous arrive d'évaluer pleinement la fonction, il n'y a pas de différence significative.
Le currying vous permet d'éviter de mentionner des paramètres pas encore nécessaires. Il est plus concis et ne nécessite pas de trouver un nom de paramètre qui n'entre pas en collision avec une autre variable de portée (ce qui est mon avantage préféré).
Par exemple, lorsque vous utilisez des fonctions qui prennent des fonctions comme arguments, vous vous retrouverez souvent dans des situations où vous avez besoin de fonctions comme "ajouter 3 à l'entrée" ou "comparer l'entrée à la variable v". Au curry, ces fonctions s'écrivent facilement: add 3
et (== v)
. Sans curry, vous devez utiliser des expressions lambda: x => add 3 x
et x => x == v
. Les expressions lambda sont deux fois plus longues et ont une petite quantité de travail occupé lié à la sélection d'un nom en plus de x
s'il y a déjà un x
dans la portée.
Un avantage secondaire des langages basés sur le curry est que, lors de l'écriture de code générique pour des fonctions, vous ne vous retrouvez pas avec des centaines de variantes en fonction du nombre de paramètres. Par exemple, en C #, une méthode 'curry' aurait besoin de variantes pour Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R>, etc. pour toujours. Dans Haskell, l'équivalent d'un Func <A1, A2, R> ressemble plus à un Func <Tuple <A1, A2>, R> ou un Func <A1, Func <A2, R >> (et un Func <R> ressemble plus à un Func <Unit, R>), donc toutes les variantes correspondent au seul cas Func <A, R>.
Le raisonnement principal auquel je peux penser (et je ne suis en aucun cas un expert en la matière) commence à montrer ses avantages à mesure que les fonctions passent de triviales à non triviales. Dans tous les cas triviaux avec la plupart des concepts de cette nature, vous ne trouverez aucun avantage réel. Cependant, la plupart des langages fonctionnels font un usage intensif de la pile dans les opérations de traitement. Considérez PostScript ou LISP comme exemples de cela. En utilisant le curry, les fonctions peuvent être empilées plus efficacement et cet avantage devient évident à mesure que les opérations deviennent de moins en moins triviales. De la manière curry, la commande et les arguments peuvent être lancés dans la pile dans l'ordre et sautés au besoin afin qu'ils soient exécutés dans le bon ordre.
Le curry dépend de façon cruciale (définitivement égale) de la capacité à renvoyer une fonction.
Considérez ce pseudo-code (artificiel).
var f = (m, x, b) => ... retourne quelque chose ...
Précisons que l'appel de f avec moins de trois arguments renvoie une fonction.
var g = f (0, 1); // cela renvoie une fonction liée à 0 et 1 (m et x) qui accepte un argument de plus (b).
var y = g (42); // invoque g avec le troisième argument manquant, en utilisant 0 et 1 pour m et x
Que vous puissiez appliquer partiellement des arguments et récupérer une fonction réutilisable (liée aux arguments que vous avez fournis) est très utile (et DRY).