Quel est l'avantage de currying?

154

Je viens d’apprendre le curry et, même si je pense comprendre le concept, je ne vois aucun avantage à l’utiliser.

Comme exemple trivial, j’utilise une fonction qui ajoute deux valeurs (écrites en ML). La version sans currying 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 qu'un sucre syntaxique qui supprime un ensemble de parenthèses de la définition et de l'appel de la fonction. J'ai vu le curry être répertorié comme l'une des caractéristiques importantes d'un langage fonctionnel, et je suis un peu décontenancé par cela pour le moment. Le concept de création d'une chaîne de fonctions consommant chacune un paramètre unique, au lieu d'une fonction prenant un tuple, semble plutôt compliqué à utiliser pour un simple changement de syntaxe.

La syntaxe légèrement plus simple est-elle la seule motivation du currying 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?

Scientifique fou
la source
54
Le curry seul est essentiellement inutile, mais avoir toutes les fonctions curry par défaut rend beaucoup d’autres fonctionnalités bien plus agréables à utiliser. Il est difficile d’apprécier cela avant d’avoir utilisé un langage fonctionnel pendant un certain temps.
CA McCann
4
Quelque chose a été mentionné en passant par delnan dans un commentaire sur la réponse de JoelEtherton, mais que je pensais mentionner explicitement, c’est que (du moins en Haskell), vous pouvez appliquer partiellement non seulement des fonctions, mais également des constructeurs de types - cela peut être assez pratique; cela pourrait être quelque chose à penser.
Paul le
Tous ont donné des exemples de Haskell. On peut se demander si le curry n’est utile qu’en Haskell.
Manoj R
@ManojR Tous n'ont pas donné d'exemples en Haskell.
phwd
1
La question a généré une discussion assez intéressante sur Reddit .
Yannis

Réponses:

126

Avec les fonctions au curry, vous obtenez une réutilisation plus facile de fonctions plus abstraites, puisque vous devez vous spécialiser. Disons que vous avez une fonction d'ajout

add x y = x + y

et que vous voulez ajouter 2 à chaque membre d'une liste. En 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 claire 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. Disons que vous avez deux fonctions de recherche. Une à partir d'une liste de paires clé / valeur et une clé à une valeur et une autre à une carte de clés à valeurs et 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é en valeur. Vous pouvez lui transmettre n'importe laquelle des fonctions de recherche ci-dessus, partiellement appliquées 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 en utilisant une syntaxe légère, puis de transférer ces fonctions partiellement appliquées à des fonctions d'ordre supérieur telles que mapou filter. Les fonctions d'ordre supérieur (qui prennent des fonctions en tant que paramètres ou les produisent en tant que résultats) constituent la base de la programmation fonctionnelle. De plus, les fonctions currying et partiellement appliquées permettent une utilisation beaucoup plus efficace et concise des fonctions d'ordre supérieur.

Boris
la source
31
Il convient de noter que, pour cette raison, l'ordre des arguments utilisé pour les fonctions dans Haskell est souvent basé sur la probabilité d'une application partielle, ce qui permet d'appliquer les avantages décrits ci-dessus (ha, ha) dans davantage de situations. Le currying par défaut finit donc par être encore plus bénéfique que ne le montrent des exemples spécifiques comme ceux-ci.
CA McCann
wat. "L'une dans une liste de paires clé / valeur et une clé pour une valeur et une autre d'une carte de clés en valeurs et une clé pour une valeur"
Mateen Ulhaq
@MateenUlhaq C'est une continuation de la phrase précédente, où je suppose que nous voulons obtenir une valeur basée sur une clé, et nous avons deux façons de le faire. La phrase énumère ces deux manières. Dans un premier temps, vous obtenez une liste de paires clé / valeur et la clé pour laquelle vous voulez trouver la valeur, et dans l'autre sens, une carte appropriée, ainsi qu'une clé. Il peut être utile de consulter le code immédiatement après la phrase.
Boris
53

La réponse pratique est que le curry facilite la création de fonctions anonymes. Même avec une syntaxe lambda minimale, c'est une victoire; comparer:

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

Si vous avez une vilaine syntaxe lambda, 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 de fonctions d'ordre supérieur dans Haskell que dans d'autres langages, j'ai constaté que j'utilisais moins la syntaxe lambda, car, dans les deux tiers des cas, le lambda serait simplement une fonction partiellement appliquée. (Et une grande partie de l'autre fois, je l'extrais 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 mappeut être écrit de deux manières:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

Lequel est le "correct"? C'est 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 associer les fonctions normales à la liste des fonctions - elle prend une fonction et retourne une fonction. Si la carte est au curry, vous n'avez pas à répondre à cette question: 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.

En outre, currying n'est vraiment pas très compliqué. C'est en fait un peu une simplification par rapport au modèle utilisé par la plupart des langages: vous n'avez besoin d'aucune notion de fonctions à arguments multiples intégrés dans votre langage. Cela reflète également de plus près le calcul lambda sous-jacent.

Bien entendu, les langages de type ML ne disposent pas de la notion d'arguments multiples sous forme curry ou non. La f(a, b, c)syntaxe correspond en fait à la transmission du tuple (a, b, c)en f, donc n'accepte ftoujours que des arguments. C’est en fait une distinction très utile que je souhaiterais que d’autres langues aient parce qu’il est très naturel d’écrire quelque chose comme:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

Vous ne pourriez pas facilement faire cela avec des langues qui ont l’idée de multiples arguments tout de suite!

Tikhon Jelvis
la source
1
"Les langages de style ML n'ont pas la notion d'arguments multiples sous forme currys ou non": à cet égard, Haskell est-il à la mode ML?
Giorgio
1
@ George: Oui.
Tikhon Jelvis
1
Intéressant. Je connais un certain Haskell et je suis en train d'apprendre la SML, il est donc intéressant de voir les différences et les similitudes entre les deux langues.
Giorgio
Excellente réponse, et si vous n'êtes toujours pas convaincu, il suffit de penser aux pipelines Unix similaires aux ruisseaux lambda
Sridhar Sarnobat
La réponse "pratique" n’a guère d’importance, car la verbosité est généralement évitée par application partielle et non par curry. Et je dirais ici que la syntaxe de l'abstraction lambda (malgré la déclaration de type) est plus laide que celle (du moins) dans Scheme car elle nécessite davantage de règles syntaxiques spéciales intégrées pour l'analyser correctement, ce qui gonfle la spécification du langage sans aucun gain. sur les propriétés sémantiques.
FrankHB
24

Le currying peut être utile si vous transmettez une fonction en tant qu'objet de première classe et que vous ne recevez pas tous les paramètres nécessaires pour l'évaluer à un endroit précis du code. Vous pouvez simplement appliquer un ou plusieurs paramètres lorsque vous les obtenez et transmettre le résultat à un autre élément de code comportant davantage de paramètres, puis terminez son évaluation.

Le code pour accomplir ceci sera plus simple que si vous devez d'abord rassembler tous les paramètres.

En outre, il est possible que le code soit réutilisé davantage, car les fonctions prenant un seul paramètre (une autre fonction curry) ne doivent pas nécessairement correspondre de manière spécifique à tous les paramètres.

psr
la source
14

La motivation principale (au moins au début) pour le currying 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 la sémantique ni pour les sémantiques des produits. Cela conduit à un langage plus simple avec autant d’expressivité qu’un autre langage plus compliqué, ce qui est souhaitable.

Alex R
la source
2
Bien que la motivation soit théorique, je pense que la simplicité est aussi presque toujours un avantage pratique. Ne pas me soucier des fonctions multi-arguments me facilite la vie quand je programme, comme si je travaillais avec la sémantique.
Tikhon Jelvis
2
@TikhonJelvis Lorsque vous programmez, le curry vous donne d'autres soucis, tels que le compilateur ne comprend pas le fait que vous avez passé trop peu d'arguments à une fonction, ou même un mauvais message d'erreur dans ce cas; lorsque vous n'utilisez pas de curry, l'erreur est beaucoup plus apparente.
Alex R
Je n'ai jamais eu de tels problèmes: GHC, à tout le moins, est très bon à cet égard. Le compilateur détecte toujours ce type de problème et a également de bons messages d'erreur pour cette erreur.
Tikhon Jelvis
1
Je ne peux pas accepter que les messages d'erreur soient qualifiés de bons. Utilisables, oui, mais ils ne sont pas encore bons. Il ne détecte également ce type de problème que s'il aboutit à une erreur de type, c'est-à-dire si vous essayez ultérieurement d'utiliser le résultat comme autre chose qu'une fonction (ou si vous avez annoté le type, mais si vous vous basez sur pour les erreurs lisibles, vous avez ses propres problèmes. ) l'emplacement signalé de l'erreur est séparé de son emplacement réel.
Alex R
14

(Je vais donner des exemples en Haskell.)

  1. Lorsque vous utilisez des langages fonctionnels, il est très pratique d'appliquer partiellement une fonction. Comme dans Haskell, il (== x)y a une fonction qui retourne Truesi son argument est égal à un terme donné x:

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    sans currying, nous aurions un code un peu moins lisible:

    mem x lst = any (\y -> y == x) lst
    
  2. Ceci est lié à la programmation tacite (voir aussi le 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 de fonctions et la manière dont l’information circule dans une chaîne de fonctions. Nous pouvons convertir notre exemple en un formulaire qui n'utilise pas de variables du tout:

    mem = any . (==)
    

    Nous considérons ici ==en fonction de ala a -> Boolet anyen fonction de a -> Boolla [a] -> Bool. En les composant simplement, nous obtenons le résultat. Tout cela grâce au currying.

  3. L'inverse, sans currying, est également utile dans certaines situations. Par exemple, supposons que nous voulions diviser une liste en deux parties - des éléments inférieurs à 10 et le reste, puis concaténer ces deux listes. La scission de la liste se fait par (ici, nous utilisons aussi le curry ). Le résultat est de type . Au lieu d'extraire le résultat dans sa première et la deuxième partie et en les combinant à l' aide , nous pouvons le faire directement par uncurrying commepartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

En effet, (uncurry (++) . partition (< 10)) [4,12,11,1]évalue à [4,1,12,11].

Il existe également d’importants avantages théoriques:

  1. Le currying est essentiel pour les langues dépourvues de types de données et ne disposant que de fonctions telles que le lambda calcul . Bien que ces langages ne soient pas utiles pour une utilisation pratique, ils sont très importants d'un point de vue théorique.
  2. Ceci est lié à la propriété essentielle des langages fonctionnels - les fonctions sont des objets de première classe. Comme nous l'avons vu, la conversion de (a, b) -> cen 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.
  3. Le (dés) currying est étroitement lié aux catégories fermées cartésiennes , ce qui est une façon catégorique de visualiser des calculs lambda typés.
Petr Pudlák
la source
Pour le bit "beaucoup moins lisible", ne devrait-il pas en être ainsi mem x lst = any (\y -> y == x) lst? (Avec une barre oblique inverse).
Stusmith
Oui, merci de le signaler, je vais le corriger.
Petr Pudlák
9

Le curry n'est pas qu'un sucre syntaxique!

Considérez les signatures de type add1(non pressé) et add2(au 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 par souci de clarté.)

add1est une fonction qui prend un 2-tuple de intet intet renvoie un int. add2est une fonction qui prend un intet retourne une autre fonction qui à son tour prend un intet retourne un int.

La différence essentielle entre les deux devient plus visible lorsque nous spécifions explicitement l'application de la fonction. Définissons une fonction (non curried) qui applique son premier argument à son second argument:

apply(f, b) = f b

Maintenant, nous pouvons voir la différence entre add1et add2plus clairement. add1se fait appeler avec un 2 tuple:

apply(add1, (3, 5))

mais add2est appelé avec un int puis sa valeur de retour est appelée avec un autreint :

apply(apply(add2, 3), 5)

EDIT: L’avantage essentiel du curry est que vous bénéficiez d’une application partielle gratuite. Disons que vous vouliez une fonction de type int -> int(disons, mapsur une liste) qui ajoute 5 à son paramètre. Vous pouvez écrire addFiveToParam x = x+5ou faire l'équivalent avec un lambda en ligne, mais vous pouvez aussi beaucoup plus facilement écrire (surtout dans les cas moins triviaux que celui-ci) add2 5!

Flamme de Ptharien
la source
3
Je comprends qu’il existe une grande différence en coulisse pour mon exemple, mais le résultat semble être un simple changement syntaxique.
Mad Scientist
5
Currying n'est pas un concept très profond. Il s'agit de simplifier le modèle sous-jacent (voir Lambda Calculus) ou dans les langages qui ont des n-uplets de toute façon, il s'agit en fait de commodité syntaxique d'application partielle. Ne sous-estimez pas l’importance de la commodité syntaxique.
Peaker
9

Le curry n'est que du sucre syntaxique, mais je pense que vous comprenez mal ce que le sucre fait. En prenant votre exemple,

fun add x y = x + y

est en fait le sucre syntaxique pour

fun add x = fn y => x + y

C'est-à-dire que (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 réalité assez différentes; ils prennent des arguments différents.

Si vous vouliez 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].

Par contre, si vous voulez additionner chaque tuple dans une liste, la fonction addTuple convient 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 de curry sont excellentes lorsqu'une application partielle est utile - par exemple map, fold, app, filter. Considérons cette fonction, qui renvoie le plus grand nombre positif de la liste fournie ou 0 s'il n'y a pas de nombres positifs:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 
gnud
la source
1
J'ai compris que la fonction au curry a une signature de type différente et qu'il s'agit en fait d'une fonction qui renvoie une autre fonction. Il me manquait cependant la partie application partielle.
Mad Scientist
9

Une autre chose que je n'ai pas encore vue mentionnée est que le curry permet une abstraction (limitée) de 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 type cpeut être un type de fonction afin que ces fonctions fonctionnent sur un préfixe de la liste de paramètres de leur argument. Sans vous lancer dans le curry, vous avez besoin d'une fonctionnalité de langage spéciale pour résumer l'arité des fonctions ou vous avez différentes versions de ces fonctions spécialisées pour différentes arités.

Geoff Reedy
la source
6

Ma compréhension limitée est telle:

1) Application de fonction partielle

Application de fonction partielle est le processus de retour d'une fonction qui prend un nombre d'arguments inférieur. Si vous fournissez 2 arguments sur 3, une fonction prenant 3-2 = 1 argument sera renvoyée. Si vous fournissez 1 argument sur 3, il retournera une fonction prenant 3-1 = 2 arguments. Si vous le souhaitez, vous pouvez même appliquer partiellement 3 arguments sur 3 et renvoyer une fonction qui ne prend aucun argument.

Donc, étant donné la fonction suivante:

f(x,y,z) = x + y + z;

En liant 1 à x et en appliquant partiellement cela à 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 liez y à 2 et z à 3 et appliquiez 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''. Donc je peux faire:

print(f''()) // and it would return 6;

ou

print(f'(1,1)) // and it would return 3;

2) curry

Le curry , d’autre part, est le processus consistant à diviser 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 faisiez curry, vous auriez 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 êtes renvoyé une nouvelle fonction:

g(y) = 1 + f''(y);

Si vous appelez g(y)avec y = 2:

g(2) = 1 + 2 + f'''(z);

Vous êtes renvoyé une nouvelle fonction:

h(z) = 1 + 2 + f'''(z);

Enfin si vous appelez h(z)avec z = 3:

h(3) = 1 + 2 + 3;

Vous revenez 6.

3) fermeture

Enfin, la fermeture est le processus de capture d’une fonction et de données en une seule unité. Une fermeture de fonction peut prendre 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 plutôt écrire une fermeture:

f(x) = x + f'(y, z);

Où:

f'(y,z) = x + y + z;

f'est fermé sur x. Signification qui f'peut lire la valeur de x qui se trouve à l'intérieur f.

Donc, si vous appelez favec x = 1:

f(1) = 1 + f'(y, z);

Vous obtiendrez une fermeture:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

Maintenant, si vous avez appelé closureOfFavec y = 2et z = 3:

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 3;

Qui reviendrait 6

Conclusion

Currying, application partielle et fermetures sont toutes assez similaires en ce sens qu'elles décomposent une fonction en plusieurs parties.

Currying décompose une fonction de plusieurs arguments en fonctions imbriquées d’arguments uniques qui renvoient des fonctions d’arguments uniques. Il ne sert à rien de traiter une fonction d'un argument ou moins, car cela n'a aucun sens.

L'application partielle décompose une fonction de plusieurs arguments en une fonction de petits arguments dont les arguments manquants ont été substitués à la valeur fournie.

Closure décompose une fonction en une fonction et un jeu de données dans lequel les variables de la fonction qui n'ont pas été passées peuvent regarder à l'intérieur du jeu de données pour trouver une valeur à lier lorsqu'elles sont invitées à évaluer.

Ce qui est déroutant à propos de tout cela, c'est qu'ils peuvent en quelque sorte être utilisés pour implémenter un sous-ensemble des autres. Donc, essentiellement, ils sont tous un peu un détail de mise en œuvre. Ils offrent tous une valeur similaire en ce sens qu'il n'est pas nécessaire de rassembler toutes les valeurs à l'avance et 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 que récemment commencé à en apprendre davantage sur ce sujet, et j’exprime donc mes connaissances actuelles, mais il pourrait y avoir des erreurs que je vous invite à signaler, et je corrigerai si / si Je découvre tout.

Didier A.
la source
1
La réponse est donc: le currying n’a pas d’avantage?
Ceving
1
@ceving Pour autant que je sache, c'est correct. En pratique, le currying et l’application partielle vous apporteront les mêmes avantages. Le choix de l'implémentation dans un langage est fait pour des raisons d'implémentation, un peut être plus facile à implémenter qu'un autre à partir d'un certain langage.
Didier A.
5

Le 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 est juste 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 des bibliothèques pour des applications partielles .

Au lieu de (lambda (x) (+ 3 x)), ce qui nous donne une fonction qui ajoute 3 à son argument, vous pouvez écrire quelque chose comme (op + 3), et donc ajouter 3 à chaque élément d'une liste serait alors (mapcar (op + 3) some-list)plutôt que (mapcar (lambda (x) (+ 3 x)) some-list). Cette opmacro fera de vous une fonction qui prend des arguments x y z ...et appelle (+ a x y z ...).

Dans de nombreux langages purement fonctionnels, une application partielle est intégrée à la syntaxe, de sorte qu'il n'y a pas d' opopérateur. Pour déclencher une application partielle, vous appelez simplement une fonction avec moins d'arguments que nécessaire. Au lieu de produire une "insufficient number of arguments"erreur, le résultat est une fonction des arguments restants.

Kaz
la source
« Corroyage ... vous permet de créer une nouvelle fonction ... en fixant certains paramètres » - non, une fonction de type a -> b -> cn'a pas le paramètre s ( au pluriel), il n'a qu'un seul paramètre, c. Lorsqu'il est appelé, il retourne une fonction de type a -> b.
Max Heiber
4

Pour la fonction

fun add(x, y) = x + y

Il est de la forme f': 'a * 'b -> 'c

Pour évaluer on va faire

add(3, 5)
val it = 8 : int

Pour la fonction au curry

fun add x y = x + y

Pour évaluer on va faire

add 3
val it = fn : int -> int

S’il s’agit d’un calcul partiel, en particulier (3 + y), auquel on peut alors compléter le calcul avec

it 5
val it = 8 : int

ajouter dans le second cas est de la forme f: 'a -> 'b -> 'c

Ce que le currying fait ici est de transformer une fonction qui prend deux accords en un seul accord qui ne donne qu'un résultat. Évaluation partielle

Pourquoi aurait-on besoin de ça?

Say xon the RHS n'est pas juste un int régulier, mais un calcul complexe qui prend un certain temps à compléter, pour augmenter, 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 twoSecondsComputationest évalué à chaque fois. Cela signifie que cela prend 6 secondes pour ce calcul.

En combinant la mise en scène et le curry, on peut é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];

Le twoSecondsComputationseul doit être évalué une fois. Pour augmenter l'échelle, remplacez deux secondes par 15 minutes ou une heure, puis créez une carte avec 100 chiffres.

Résumé : Le curry est excellent lorsque vous utilisez d’autres méthodes pour les fonctions de niveau supérieur en tant qu’outil d’évaluation partielle. Son but ne peut pas vraiment être démontré par lui-même.

phwd
la source
3

Le curry permet une composition fonctionnelle flexible.

J'ai composé une fonction "curry". Dans ce contexte, je me fiche de quel type d’enregistreur je reçois ou d’où il provient. Je me fiche de l'action ou de sa provenance. Tout ce qui m'importe, c'est de traiter mes entrées.

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 retourne une fonction qui retourne une fonction prenant mon entrée qui fait mon travail. Ceci est un exemple simple et utile et non un objet en vue.

mortalapeman
la source
2

Le curry est un avantage lorsque vous n'avez pas tous les arguments pour une fonction. Si vous évaluez pleinement la fonction, il n'y a pas de différence significative.

Le currying vous permet d'éviter de mentionner des paramètres qui ne sont pas encore nécessaires. Il est plus concis et n'exige pas de trouver un nom de paramètre qui n'entre pas en conflit avec une autre variable dans la portée (ce qui est mon avantage préféré).

Par exemple, lorsque vous utilisez des fonctions qui prennent des fonctions en arguments, vous vous retrouvez souvent dans des situations où vous avez besoin de fonctions telles que "ajouter 3 à l'entrée" ou "comparer l'entrée à la variable v". Avec currying, ces fonctions s’écrit facilement: add 3et (== v). Sans currying, vous devez utiliser des expressions lambda: x => add 3 xet x => x == v. Les expressions lambda sont deux fois plus longues et nécessitent un peu de travail lié à la sélection d'un nom, xsi ce n'est déjà le cas x.

Un avantage supplémentaire des langages basés sur le currying est que, lors de l'écriture de code générique pour des fonctions, vous ne vous retrouvez pas avec des centaines de variantes basées sur le 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. En Haskell, l’équivalent d’un Func <A1, A2, R> ressemble davantage à un Func <Tuple <A1, A2>, R> ou à un Func <A1, Func <A2, R >> (et à un Func <R> ressemble plus à un Func <Unité, R>), de sorte que toutes les variantes correspondent au cas unique Func <A, R>.

Craig Gidney
la source
2

Le raisonnement principal auquel je peux penser (et je ne suis en aucun cas un expert sur ce sujet) commence à montrer ses avantages lorsque les fonctions passent de trivial à non trivial. Dans tous les cas triviaux contenant la plupart des concepts de cette nature, vous ne trouverez aucun avantage réel. Cependant, la plupart des langages fonctionnels utilisent beaucoup la pile dans les opérations de traitement. Considérez PostScript ou Lisp comme exemples. 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 manière curry, la commande et les arguments peuvent être lancés sur la pile dans l'ordre et supprimés au besoin afin qu'ils soient exécutés dans le bon ordre.

Peter Mortensen
la source
1
En quoi le fait de requérir beaucoup plus de cadres de pile pour améliorer l'efficacité des choses?
Mason Wheeler
1
@MasonWheeler: Je ne saurais vous dire, comme je l'ai dit, je ne suis pas un expert des langages fonctionnels ou du curry en particulier. J'ai étiqueté ce wiki de communauté spécifiquement à cause de cela.
Joel Etherton
4
@MasonWheeler Vous avez quelque chose à dire sur le libellé de cette réponse, mais permettez-moi de dire que la quantité de trames de pile réellement créées dépend beaucoup de la mise en œuvre. Par exemple, dans la machine G sans étiquette sans rotation (STG; la façon dont GHC implémente Haskell) retarde l’évaluation réelle jusqu’à ce qu’elle accumule tous les arguments (ou du moins autant qu’elle sait être requise). Je n'arrive pas à me rappeler si cela est fait pour toutes les fonctions ou seulement pour les constructeurs, mais je pense que cela devrait être possible pour la plupart des fonctions. (Là encore, le concept de "cadres de pile" ne s'applique pas vraiment au STG.)
1

Le curry dépend de manière décisive (définitivement même) de la capacité de retourner une fonction.

Considérez ce pseudo-code (artificiel).

var f = (m, x, b) => ... renvoie quelque chose ...

Disons que l'appel de f avec moins de trois arguments renvoie une fonction.

var g = f (0, 1); // this renvoie une fonction liée à 0 et 1 (m et x) qui accepte un argument supplémentaire (b).

var y = g (42); // invoque g avec le troisième argument manquant, en utilisant 0 et 1 pour m et x

Le fait que vous puissiez partiellement appliquer des arguments et récupérer une fonction réutilisable (liée aux arguments que vous avez fournis) est très utile (et DRY).

Rick O'Shea
la source