Qual è il vantaggio del curry?


154

Ho appena imparato a conoscere il curry e mentre penso di aver capito il concetto, non vedo alcun grande vantaggio nell'usarlo.

Come esempio banale uso una funzione che aggiunge due valori (scritti in ML). La versione senza curry sarebbe

fun add(x, y) = x + y

e sarebbe chiamato come

add(3, 5)

mentre la versione al curry è

fun add x y = x + y 
(* short for val add = fn x => fn y=> x + y *)

e sarebbe chiamato come

add 3 5

Mi sembra solo lo zucchero sintattico che rimuove una serie di parentesi dalla definizione e dal richiamo della funzione. Ho visto il curry elencato come una delle caratteristiche importanti di un linguaggio funzionale e al momento ne sono un po 'deluso. Il concetto di creare una catena di funzioni che utilizza ciascuna un singolo parametro, anziché una funzione che prende una tupla, sembra piuttosto complicato da usare per un semplice cambio di sintassi.

La sintassi leggermente più semplice è l'unica motivazione per il curry o mi sto perdendo altri vantaggi che non sono ovvi nel mio esempio molto semplice? Il curry è solo zucchero sintattico?


54
Currying da solo è essenzialmente inutile, ma avere tutte le funzioni curate di default rende molte altre funzioni molto più piacevoli da usare. È difficile apprezzarlo fino a quando non usi un linguaggio funzionale per un po '.
CA McCann,

4
Qualcosa che è stato menzionato di passaggio da Delnan in un commento sulla risposta di JoelEtherton, ma che ho pensato di menzionare esplicitamente, è che (almeno in Haskell) puoi applicare parzialmente non solo le funzioni ma anche i costruttori di tipi - questo può essere abbastanza maneggevole; questo potrebbe essere qualcosa su cui riflettere.
paul

Tutti hanno fornito esempi di Haskell. Ci si potrebbe chiedere che il curry sia utile solo in Haskell.
Manoj R,

@ManojR Tutti non hanno fornito esempi in Haskell.
phwd,

1
La domanda ha generato una discussione abbastanza interessante su Reddit .
yannis,

Risposte:


126

Con le funzioni al curry puoi riutilizzare più facilmente le funzioni più astratte, poiché puoi specializzarti. Diciamo che hai una funzione aggiunta

add x y = x + y

e che si desidera aggiungere 2 a ogni membro di un elenco. In Haskell faresti questo:

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

Qui la sintassi è più leggera che se dovessi creare una funzione add2

add2 y = add 2 y
map add2 [1, 2, 3]

o se hai dovuto fare una funzione lambda anonima:

map (\y -> 2 + y) [1, 2, 3]

Ti consente anche di astrarre lontano da diverse implementazioni. Supponiamo che tu abbia avuto due funzioni di ricerca. Uno da un elenco di coppie chiave / valore e una chiave per un valore e un altro da una mappa da chiavi a valori e una chiave per un valore, come questo:

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

Quindi è possibile creare una funzione che accetta una funzione di ricerca da Chiave a Valore. Potresti passare una qualsiasi delle funzioni di ricerca sopra, parzialmente applicate con un elenco o una mappa, rispettivamente:

myFunc :: (Key -> Value) -> .....

In conclusione: il curry è buono, perché ti consente di specializzare / applicare parzialmente le funzioni usando una sintassi leggera e quindi passare queste funzioni parzialmente applicate a funzioni di ordine superiore come mapo filter. Le funzioni di ordine superiore (che assumono funzioni come parametri o le danno come risultati) sono il pane e il burro della programmazione funzionale, e le funzioni di curry e parzialmente applicate consentono di utilizzare le funzioni di ordine superiore in modo molto più efficace e conciso.


31
Vale la pena notare che, per questo motivo, l'ordine degli argomenti utilizzato per le funzioni in Haskell si basa spesso sulla probabilità della parziale applicazione, che a sua volta applica i vantaggi sopra descritti (ah, ah) in più situazioni. Currying di default finisce quindi per essere ancora più vantaggioso di quanto sia evidente da esempi specifici come quelli qui.
CA McCann,

wat. "Uno da un elenco di coppie chiave / valore e una chiave per un valore e un altro da una mappa da chiavi a valori e una chiave per un valore"
Mateen Ulhaq,

@MateenUlhaq È una continuazione della frase precedente, in cui suppongo che vogliamo ottenere un valore basato su una chiave e abbiamo due modi per farlo. La frase elenca questi due modi. Nel primo modo, ti viene fornito un elenco di coppie chiave / valore e la chiave per la quale vogliamo trovare il valore, e nell'altro modo ci viene data una mappa corretta, e di nuovo una chiave. Potrebbe essere utile esaminare il codice immediatamente dopo la frase.
Boris,

53

La risposta pratica è che il curry rende molto più semplice la creazione di funzioni anonime. Anche con una sintassi lambda minima, è una sorta di vittoria; confrontare:

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

Se hai una brutta sintassi lambda, è anche peggio. (Ti sto guardando, JavaScript, Scheme e Python.)

Questo diventa sempre più utile quando si utilizzano sempre più funzioni di ordine superiore. Mentre uso più funzioni di ordine superiore in Haskell che in altre lingue, ho scoperto che in realtà uso meno la sintassi lambda perché qualcosa come due terzi del tempo, la lambda sarebbe solo una funzione parzialmente applicata. (E molte altre volte lo estraggo in una funzione con nome.)

Più fondamentalmente, non è sempre ovvio quale versione di una funzione sia "canonica". Ad esempio, prendi map. Il tipo di mappuò essere scritto in due modi:

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

Qual è quello "corretto"? In realtà è difficile da dire. In pratica, la maggior parte delle lingue utilizza la prima: la mappa accetta una funzione e un elenco e restituisce un elenco. Tuttavia, fondamentalmente, ciò che la mappa effettivamente fa è mappare le normali funzioni per elencarle - ci vuole una funzione e restituisce una funzione. Se la mappa è al curry, non devi rispondere a questa domanda: fa entrambe le cose , in un modo molto elegante.

Ciò diventa particolarmente importante una volta generalizzato mapa tipi diversi dall'elenco.

Inoltre, il curry non è davvero molto complicato. In realtà è un po 'una semplificazione rispetto al modello utilizzato dalla maggior parte delle lingue: non è necessaria alcuna nozione di funzioni di più argomenti incorporate nella tua lingua. Ciò riflette anche più da vicino il calcolo lambda sottostante.

Naturalmente, i linguaggi in stile ML non hanno una nozione di argomenti multipli in forma curry o non fretta. La f(a, b, c)sintassi corrisponda effettivamente passando nella tupla (a, b, c)in fmodo fancora richiede solo sull'argomento. Questa è in realtà una distinzione molto utile che vorrei che altre lingue avrebbero perché rende molto naturale scrivere qualcosa del tipo:

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

Non puoi farlo facilmente con le lingue che hanno l'idea di più argomenti cotti proprio in!


1
"I linguaggi in stile ML non hanno una nozione di argomenti multipli in forma curry o non frettolosa": a questo proposito, è in stile ML Haskell?
Giorgio,

1
@Giorgio: Sì.
Tikhon Jelvis,

1
Interessante. Conosco un po 'di Haskell e sto imparando SML in questo momento, quindi è interessante vedere differenze e somiglianze tra le due lingue.
Giorgio,

Ottima risposta, e se non sei ancora convinto, pensa solo alle condotte Unix che sono simili ai flussi lambda
Sridhar Sarnobat,

La risposta "pratica" non è molto rilevante perché la verbosità viene solitamente evitata da un'applicazione parziale , non dal curry. E direi qui che la sintassi dell'astrazione lambda (nonostante la dichiarazione del tipo) è più brutta di quella (almeno) nello Schema in quanto ha bisogno di regole sintattiche speciali più integrate per analizzarlo correttamente, che gonfia le specifiche del linguaggio senza alcun guadagno sulle proprietà semantiche.
FrankHB,

24

Currying può essere utile se si dispone di una funzione che si sta passando come oggetto di prima classe e non si ricevono tutti i parametri necessari per valutarlo in un'unica posizione nel codice. Puoi semplicemente applicare uno o più parametri quando li ottieni e passare il risultato a un altro pezzo di codice che ha più parametri e finire di valutarlo lì.

Il codice per raggiungere questo obiettivo sarà più semplice rispetto a quando è necessario prima riunire tutti i parametri.

Inoltre, esiste la possibilità di riutilizzare più codice, poiché le funzioni che accettano un singolo parametro (un'altra funzione al curry) non devono corrispondere in modo specifico a tutti i parametri.


14

La motivazione principale (almeno inizialmente) per il curry non era pratica ma teorica. In particolare, il curry consente di ottenere in modo efficace funzioni multi-argomento senza effettivamente definire la semantica per loro o definire la semantica per i prodotti. Ciò porta a un linguaggio più semplice con la stessa espressività di un altro linguaggio più complicato, e quindi è desiderabile.


2
Mentre la motivazione qui è teorica, penso che la semplicità sia quasi sempre anche un vantaggio pratico. Non preoccuparmi delle funzioni multi-argomento mi semplifica la vita quando programmo, proprio come farei con la semantica.
Tikhon Jelvis,

2
@TikhonJelvis Quando stai programmando, però, il curry ti dà altre cose di cui preoccuparti, come il compilatore che non capisce il fatto che hai passato troppi argomenti a una funzione o che in quel caso ricevi anche un brutto messaggio di errore; quando non si utilizza il curry, l'errore è molto più evidente.
Alex R

Non ho mai avuto problemi del genere: GHC, per lo meno, è molto bravo in questo senso. Il compilatore rileva sempre questo tipo di problema e presenta anche buoni messaggi di errore per questo errore.
Tikhon Jelvis,

1
Non posso essere d'accordo sul fatto che i messaggi di errore siano validi. Utile, sì, ma non sono ancora buoni. Inoltre rileva questo tipo di problema solo se si verifica un errore di tipo, ovvero se in seguito si tenta di utilizzare il risultato come qualcosa di diverso da una funzione (o se si digita annotato, ma fare affidamento su di esso per errori leggibili ha i suoi problemi ); la posizione dell'errore segnalata è separata dalla sua posizione effettiva.
Alex R

14

(Darò esempi in Haskell.)

  1. Quando si usano linguaggi funzionali, è molto comodo applicare parzialmente una funzione. Come in Haskell (== x)c'è una funzione che ritorna Truese il suo argomento è uguale a un dato termine x:

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

    senza curry, avremmo un codice un po 'meno leggibile:

    mem x lst = any (\y -> y == x) lst
    
  2. Ciò è correlato alla programmazione Tacit (vedi anche lo stile Pointfree sul wiki Haskell). Questo stile non si concentra su valori rappresentati da variabili, ma sulla composizione di funzioni e su come le informazioni fluiscono attraverso una catena di funzioni. Possiamo convertire il nostro esempio in un modulo che non utilizza affatto le variabili:

    mem = any . (==)
    

    Qui vediamo ==come una funzione da aa a -> Boole anycome una funzione da a -> Boola [a] -> Bool. Semplicemente componendoli, otteniamo il risultato. Questo grazie al curry.

  3. Il contrario, non curry, è utile anche in alcune situazioni. Ad esempio, supponiamo di voler dividere un elenco in due parti: elementi più piccoli di 10 e il resto, quindi concatenare quei due elenchi. La suddivisione dell'elenco viene eseguita da (qui usiamo anche il curry ). Il risultato è di tipo . Invece di estrarre il risultato nella sua prima e la seconda parte e la loro combinazione attraverso , possiamo farlo direttamente uncurrying comepartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

Anzi, (uncurry (++) . partition (< 10)) [4,12,11,1]valuta [4,1,12,11].

Ci sono anche importanti vantaggi teorici:

  1. Il curriculum è essenziale per le lingue che mancano di tipi di dati e hanno solo funzioni, come il calcolo lambda . Sebbene queste lingue non siano utili per l'uso pratico, sono molto importanti da un punto di vista teorico.
  2. Ciò è collegato alla proprietà essenziale dei linguaggi funzionali: le funzioni sono oggetti di prima classe. Come abbiamo visto, la conversione da (a, b) -> cin a -> (b -> c)significa che il risultato di quest'ultima funzione è di tipo b -> c. In altre parole, il risultato è una funzione.
  3. Il curriculum (Un) è strettamente collegato alle categorie chiuse cartesiane , che è un modo categorico di visualizzare i calcoli lambda digitati.

Per il bit "codice molto meno leggibile", non dovrebbe essere mem x lst = any (\y -> y == x) lst? (Con una barra rovesciata).
stusmith

Sì, grazie per averlo sottolineato, lo correggerò.
Petr Pudlák,

9

Currying non è solo zucchero sintattico!

Considera le firme dei tipi di add1(non fretta) e add2(curry):

add1 : (int * int) -> int
add2 : int -> (int -> int)

(In entrambi i casi, le parentesi nella firma del tipo sono opzionali, ma le ho incluse per motivi di chiarezza.)

add1è una funzione che prende un 2-tupla inted inte restituisce un int. add2è una funzione che accetta un inte restituisce un'altra funzione che a sua volta prende un inte restituisce un int.

La differenza essenziale tra i due diventa più visibile quando specifichiamo esplicitamente l'applicazione di funzioni. Definiamo una funzione (non curry) che applica il suo primo argomento al secondo argomento:

apply(f, b) = f b

Ora possiamo vedere la differenza tra add1e add2più chiaramente. add1viene chiamato con una 2 tupla:

apply(add1, (3, 5))

ma add2viene chiamato con un int e quindi il suo valore di ritorno viene chiamato con un altroint :

apply(apply(add2, 3), 5)

EDIT: il vantaggio essenziale del curry è che si ottiene un'applicazione parziale gratuitamente. Diciamo che volevi una funzione di tipo int -> int(diciamo ad mapesso su un elenco) che ha aggiunto 5 al suo parametro. Potresti scrivere addFiveToParam x = x+5, o potresti fare l'equivalente con un lambda in linea, ma potresti anche scrivere molto più facilmente (specialmente in casi meno banali di questo) add2 5!


3
Capisco che c'è una grande differenza dietro le quinte per il mio esempio, ma il risultato sembra essere un semplice cambiamento sintattico.
Mad Scientist,

5
Currying non è un concetto molto profondo. Si tratta di semplificare il modello sottostante (vedi Lambda Calculus) o in linguaggi che hanno comunque tuple, in realtà si tratta di convenienza sintattica dell'applicazione parziale. Non sottovalutare l'importanza della convenienza sintattica.
Peaker il

9

Currying è solo zucchero sintattico, ma tu stai leggermente fraintendendo quello che fa lo zucchero, credo. Prendendo il tuo esempio,

fun add x y = x + y

è in realtà zucchero sintattico per

fun add x = fn y => x + y

Cioè, (aggiungi x) restituisce una funzione che accetta un argomento y e aggiunge x a y.

fun addTuple (x, y) = x + y

Questa è una funzione che prende una tupla e aggiunge i suoi elementi. Queste due funzioni sono in realtà abbastanza diverse; prendono argomenti diversi.

Se si desidera aggiungere 2 a tutti i numeri in un elenco:

(* 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]

Il risultato sarebbe [3,4,5].

Se vuoi sommare ogni tupla in un elenco, d'altra parte, la funzione addTuple si adatta perfettamente.

(* 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)]

Il risultato sarebbe [12,13,14].

Le funzioni al curry sono ottime dove è utile un'applicazione parziale, ad esempio mappa, piega, app, filtro. Considera questa funzione, che restituisce il numero positivo più grande nell'elenco fornito o 0 se non ci sono numeri positivi:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 

1
Ho capito che la funzione curry ha una firma di tipo diverso e che in realtà è una funzione che restituisce un'altra funzione. Mi mancava però la parte parziale dell'applicazione.
Mad Scientist,

9

Un'altra cosa che non ho ancora visto è che il curry consente l'astrazione (limitata) sull'arte.

Considera queste funzioni che fanno parte della libreria di 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

In ogni caso la variabile type cpuò essere un tipo di funzione in modo che queste funzioni agiscano su alcuni prefissi dell'elenco dei parametri del loro argomento. Senza curry, avresti bisogno di una caratteristica del linguaggio speciale per astrarre sull'arità delle funzioni o avere molte versioni diverse di queste funzioni specializzate per le diverse arità.


6

La mia comprensione limitata è tale:

1) Applicazione con funzioni parziali

L'applicazione di funzione parziale è il processo di restituzione di una funzione che accetta un numero minore di argomenti. Se si forniscono 2 argomenti su 3, verrà restituita una funzione che accetta 3-2 = 1 argomento. Se si forniscono 1 su 3 argomenti, verrà restituita una funzione che accetta 3-1 = 2 argomenti. Se lo desideri, potresti anche applicare parzialmente 3 argomenti su 3 e restituirebbe una funzione che non accetta argomenti.

Quindi, data la seguente funzione:

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

Quando si associa 1 a x e si applica parzialmente quello alla funzione sopra f(x,y,z)si otterrà:

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

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

Ora se dovessi legare y a 2 e z a 3 e applicare parzialmente f'(y,z)otterrai:

f'(2,3) = f''();

Dove f''() = 1 + 2 + 3:;

Ora in qualsiasi momento, è possibile scegliere di valutare f, f'o f''. Quindi posso fare:

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

o

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

2) Currying

D' altra parte, il processo di suddivisione di una funzione in una catena nidificata di funzioni di un argomento è al contrario. Non puoi mai fornire più di 1 argomento, è uno o zero.

Quindi, data la stessa funzione:

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

Se lo mettessi al curry, otterrai una catena di 3 funzioni:

f'(x) -> f''(y) -> f'''(z)

Dove:

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

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

f'''(z) = z;

Ora se chiami f'(x)con x = 1:

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

Ti viene restituita una nuova funzione:

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

Se chiami g(y)con y = 2:

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

Ti viene restituita una nuova funzione:

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

Infine se chiami h(z)con z = 3:

h(3) = 1 + 2 + 3;

Sei tornato 6.

3) Chiusura

Infine, la chiusura è il processo di acquisizione di una funzione e di dati come un'unica unità. Una chiusura di una funzione può portare da 0 a un numero infinito di argomenti, ma è anche consapevole dei dati che non le sono stati passati.

Ancora una volta, data la stessa funzione:

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

Puoi invece scrivere una chiusura:

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

Dove:

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

f'è chiuso il x. Significa che f'può leggere il valore di x che è dentro f.

Quindi se dovessi chiamare fcon x = 1:

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

Avresti una chiusura:

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

Ora se hai chiamato closureOfFcon y = 2e z = 3:

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

Che sarebbe tornato 6

Conclusione

Currying, applicazione parziale e chiusure sono in qualche modo simili in quanto scompongono una funzione in più parti.

Currying decompone una funzione di più argomenti in funzioni nidificate di singoli argomenti che restituiscono funzioni di singoli argomenti. Non ha senso valutare una funzione di uno o meno argomenti, dal momento che non ha senso.

L'applicazione parziale decompone una funzione di più argomenti in una funzione di argomenti minori i cui argomenti ora mancanti sono stati sostituiti con il valore fornito.

La chiusura decompone una funzione in una funzione e un set di dati in cui le variabili all'interno della funzione che non sono state passate possono guardare all'interno del set di dati per trovare un valore a cui associarsi quando viene chiesto di valutare.

La cosa che confonde di tutto ciò è che possono essere usati per implementare un sottoinsieme degli altri. Quindi, in sostanza, sono tutti un po 'dettagli di implementazione. Forniscono tutti un valore simile in quanto non è necessario raccogliere tutti i valori in anticipo e in quanto è possibile riutilizzare parte della funzione, poiché è stata scomposta in unità discrete.

Divulgazione

Non sono affatto un esperto dell'argomento, solo recentemente ho iniziato a conoscerli e quindi fornisco la mia attuale comprensione, ma potrebbe avere errori che ti invito a sottolineare e correggerò come / se Ne scopro qualcuno.


1
Quindi la risposta è: il curry non ha alcun vantaggio?
ceving il

1
@ceving Per quanto ne so, è corretto. In pratica il curry e l'applicazione parziale ti daranno gli stessi vantaggi. La scelta di quale implementare in una lingua è fatta per motivi di implementazione, uno potrebbe essere più facile da implementare rispetto a un altro dato un determinato linguaggio.
Didier A.

5

Currying (applicazione parziale) consente di creare una nuova funzione da una funzione esistente fissando alcuni parametri. È un caso speciale di chiusura lessicale in cui la funzione anonima è solo un banale wrapper che passa alcuni argomenti acquisiti a un'altra funzione. Possiamo anche farlo usando la sintassi generale per fare chiusure lessicali, ma un'applicazione parziale fornisce uno zucchero sintattico semplificato.

Questo è il motivo per cui i programmatori Lisp, quando lavorano in uno stile funzionale, a volte usano le librerie per un'applicazione parziale .

Invece di (lambda (x) (+ 3 x)), che ci dà una funzione che aggiunge 3 al suo argomento, puoi scrivere qualcosa di simile (op + 3), e quindi aggiungere 3 a ogni elemento di un certo elenco sarebbe quindi (mapcar (op + 3) some-list)piuttosto che (mapcar (lambda (x) (+ 3 x)) some-list). Questa opmacro ti renderà una funzione che accetta alcuni argomenti x y z ...e invoca (+ a x y z ...).

In molti linguaggi puramente funzionali, l'applicazione parziale è radicata nella sintassi in modo che non vi siano opoperatori. Per attivare un'applicazione parziale, è sufficiente chiamare una funzione con meno argomenti di quanti ne richieda. Invece di produrre un "insufficient number of arguments"errore, il risultato è una funzione degli argomenti rimanenti.


"Accattivarsi ... consente di creare una nuova funzione ... fissando alcuni parametri" - no, in funzione del tipo a -> b -> cnon ha parametro s (plurale), che ha un solo parametro, c. Quando viene chiamato, restituisce una funzione di tipo a -> b.
Max Heiber,

4

Per la funzione

fun add(x, y) = x + y

È della forma f': 'a * 'b -> 'c

Per valutare uno farà

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

Per la funzione al curry

fun add x y = x + y

Per valutare uno farà

add 3
val it = fn : int -> int

Dove è un calcolo parziale, in particolare (3 + y), con il quale si può completare il calcolo

it 5
val it = 8 : int

aggiungere nel secondo caso è del modulo f: 'a -> 'b -> 'c

Ciò che sta facendo il curry qui è trasformare una funzione che accetta due accordi in uno che richiede solo uno che restituisce un risultato. Valutazione parziale

Perché uno dovrebbe averne bisogno?

Dire xsu RHS non è solo un normale int, ma invece un complesso calcolo che richiede un po 'di tempo per completare, per motivi di interesse, due secondi.

x = twoSecondsComputation(z)

Quindi la funzione ora sembra

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

Di tipo add : int * int -> int

Ora vogliamo calcolare questa funzione per un intervallo di numeri, mappiamola

val result1 = map (fn x => add (20, x)) [3, 5, 7];

Per quanto sopra il risultato di twoSecondsComputationviene valutato ogni volta. Ciò significa che ci vogliono 6 secondi per questo calcolo.

Utilizzando una combinazione di messa in scena e curry si può evitare questo.

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

Della forma al curry add : int -> int -> int

Ora si può fare

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

L' twoSecondsComputationunica deve essere valutata una volta. Per aumentare la scala, sostituisci due secondi con 15 minuti o qualsiasi ora, quindi disponi di una mappa con 100 numeri.

Riepilogo : Currying è ottimo quando si utilizza con altri metodi per funzioni di livello superiore come strumento di valutazione parziale. Il suo scopo non può davvero essere dimostrato da solo.


3

Currying consente una composizione flessibile delle funzioni.

Ho inventato una funzione "curry". In questo contesto, non mi interessa che tipo di logger ricevo o da dove provenga. Non mi interessa quale sia l'azione o da dove provenga. Tutto quello che mi interessa è l'elaborazione dei miei input.

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 variabile builder è una funzione che restituisce una funzione che restituisce una funzione che accetta il mio input e fa il mio lavoro. Questo è un semplice esempio utile e non un oggetto in vista.


2

Currying è un vantaggio quando non si hanno tutti gli argomenti per una funzione. Se ti capita di valutare completamente la funzione, allora non ci sono differenze significative.

Currying ti consente di evitare di menzionare i parametri non ancora necessari. È più conciso e non richiede di trovare un nome di parametro che non si scontri con un'altra variabile nell'ambito (che è il mio vantaggio preferito).

Ad esempio, quando si utilizzano funzioni che accettano funzioni come argomenti, spesso ci si trova in situazioni in cui sono necessarie funzioni come "aggiungi 3 all'input" o "confronta l'input con la variabile v". Con il curry, queste funzioni sono facilmente scritte: add 3e (== v). Senza curry, devi usare le espressioni lambda: x => add 3 xe x => x == v. Le espressioni lambda sono lunghe il doppio e hanno una piccola quantità di lavoro occupato correlato alla scelta di un nome oltre a xse esiste già un xambito.

Un vantaggio collaterale delle lingue basate sul curry è che, quando si scrive codice generico per funzioni, non si finisce con centinaia di varianti basate sul numero di parametri. Ad esempio, in C #, un metodo 'curry' avrebbe bisogno di varianti per Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R> e così via per sempre. In Haskell, l'equivalente di un Func <A1, A2, R> è più simile a un Func <Tupla <A1, A2>, R> o un Func <A1, Func <A2, R >> (e un Func <R> è più simile a Func <Unità, R>), quindi tutte le varianti corrispondono al singolo caso Func <A, R>.


2

Il ragionamento principale che mi viene in mente (e non sono affatto un esperto in materia) inizia a mostrare i suoi benefici mentre le funzioni passano da banali a non banali. In tutti i casi banali con la maggior parte dei concetti di questa natura non troverai alcun reale beneficio. Tuttavia, la maggior parte dei linguaggi funzionali fa un uso pesante dello stack nelle operazioni di elaborazione. Considera PostScript o Lisp come esempi di questo. Usando il curry, le funzioni possono essere impilate in modo più efficace e questo vantaggio diventa evidente quando le operazioni diventano sempre meno banali. Nel modo più veloce, il comando e gli argomenti possono essere lanciati nello stack in ordine e saltati via se necessario in modo che vengano eseguiti nell'ordine corretto.


1
In che modo esattamente la richiesta di creare più stack frame rende le cose più efficienti?
Mason Wheeler,

1
@MasonWheeler: non lo saprei, come ho detto, non sono un esperto di linguaggi funzionali o di curry in particolare. Ho etichettato questa wiki della community proprio per questo.
Joel Etherton,

4
@MasonWheeler Hai ragione a scrivere il fraseggio di questa risposta, ma lasciami entrare e dire che la quantità di frame di stack effettivamente creati dipende molto dall'implementazione. Ad esempio, nella macchina G senza tag senza spin (STG; il modo in cui GHC implementa Haskell) ritarda la valutazione effettiva fino a quando non accumula tutti (o almeno tutti quelli che sa essere richiesti). Non riesco a ricordare se questo è fatto per tutte le funzioni o solo per i costruttori, ma penso che dovrebbe essere possibile per la maggior parte delle funzioni. (Ancora una volta, il concetto di "stack frame" non si applica realmente allo STG.)

1

Currying dipende in modo cruciale (definitivamente uniforme) dalla capacità di restituire una funzione.

Considera questo pseudo codice (inventato).

var f = (m, x, b) => ... restituisce qualcosa ...

Supponiamo che chiamare f con meno di tre argomenti restituisca una funzione.

var g = f (0, 1); // questo restituisce una funzione associata a 0 e 1 (m e x) che accetta un altro argomento (b).

var y = g (42); // invoca g con il terzo argomento mancante, usando 0 e 1 per m e x

Che sia possibile applicare parzialmente argomenti e recuperare una funzione riutilizzabile (legata a quegli argomenti forniti) è abbastanza utile (e DRY).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.