Cosa c'è di speciale nel curry o nell'applicazione parziale?


9

Ho letto articoli sulla programmazione funzionale ogni giorno e ho cercato di applicare alcune pratiche il più possibile. Ma non capisco cosa sia unico nel curry o in un'applicazione parziale.

Prendi questo codice Groovy come esempio:

def mul = { a, b -> a * b }
def tripler1 = mul.curry(3)
def tripler2 = { mul(3, it) }

Non capisco qual è la differenza tra tripler1e tripler2. Non sono entrambi uguali? Il "curry" è supportato in linguaggi funzionali puri o parziali come Groovy, Scala, Haskell ecc. Ma posso fare la stessa cosa (curry di sinistra, curry di destra, n-curry o applicazione parziale) semplicemente creando un altro nome o anonimo funzione o chiusura che inoltrerà i parametri alla funzione originale (come tripler2) nella maggior parte delle lingue (anche C.)

Mi sto perdendo qualcosa qui? Ci sono posti in cui posso usare il curry e l'applicazione parziale nella mia applicazione Grails, ma esito a farlo perché mi chiedo "Com'è diverso?"

Per favore, illuminami.

EDIT: State dicendo che l'applicazione parziale / curry è semplicemente più efficiente della creazione / chiamata di un'altra funzione che inoltra i parametri predefiniti alla funzione originale?


1
Qualcuno può creare i tag "curry" o "curry"?
Vigneshwaran,

Come si fa a curry in C?
Giorgio,

questo è probabilmente molto più relativo ai programmatori di
jk.

1
@Vigneshwaran: AFAIK non è necessario creare un'altra funzione in una lingua a supporto del curry. Ad esempio, in Haskell f x y = x + ysignifica che fè una funzione che accetta un parametro int. Il risultato di f x( fapplicato a x) è una funzione che accetta un parametro int. Il risultato f x y(o (f x) y, cioè f xapplicato a y) è un'espressione che non accetta parametri di input e viene valutata riducendo x + y.
Giorgio,

1
Puoi ottenere le stesse cose, ma la quantità di sforzo che fai con C è molto più dolorosa e non efficiente come in una lingua come l'hashell in cui è il comportamento predefinito
Daniel Gratzer,

Risposte:


8

Currying consiste nel trasformare / rappresentare una funzione che accetta n input in n funzioni che ciascuna accetta 1 input. L'applicazione parziale riguarda il fissaggio di alcuni degli input a una funzione.

La motivazione per un'applicazione parziale è principalmente che semplifica la scrittura di librerie di funzioni di ordine superiore. Ad esempio, gli algoritmi in C ++ STL assumono in gran parte predicati o funzioni unarie, bind1st consente all'utente della libreria di agganciarsi a funzioni non unarie con un valore associato. Pertanto, il writer della libreria non deve fornire funzioni sovraccaricate per tutti gli algoritmi che accettano funzioni unarie per fornire versioni binarie

Currying stesso è utile perché ti dà un'applicazione parziale ovunque tu voglia gratuitamente, cioè non hai più bisogno di una funzione come bind1stapplicare parzialmente.


è curryingqualcosa di specifico per groovy o applicabile attraverso le lingue?
anfibio,

@foampile è qualcosa che è applicabile in tutte le lingue, ma ironicamente il curry groovy non lo fa davvero programmers.stackexchange.com/questions/152868/…
jk.

@jk. Stai dicendo che l'applicazione curry / parziale è più efficiente della creazione e della chiamata di un'altra funzione?
Vigneshwaran,

2
@Vigneshwaran - non è necessariamente più performante, ma è sicuramente più efficiente in termini di tempo del programmatore. Si noti inoltre che mentre il curry è supportato da molti linguaggi funzionali, ma generalmente non è supportato in OO o linguaggi procedurali. (O almeno, non dalla lingua stessa.)
Stephen C,

6

Ma posso fare la stessa cosa (curry di sinistra, curry di destra, n-curry o applicazione parziale) semplicemente creando un'altra funzione o chiusura denominata o anonima che inoltrerà i parametri alla funzione originale (come tripler2) nella maggior parte delle lingue ( anche C.)

E l'ottimizzatore lo esaminerà e passerà prontamente a qualcosa che può capire. Currying è un piccolo accorgimento per l'utente finale, ma offre vantaggi molto migliori dal punto di vista della progettazione linguistica. È davvero bello gestire tutti i metodi come unari, A -> Bdove Bpotrebbe esserci un altro metodo.

Semplifica i metodi da scrivere per gestire le funzioni di ordine superiore. L'analisi statica e l'ottimizzazione nella lingua ha solo un percorso con cui lavorare che si comporta in modo noto. L'associazione dei parametri non rientra nella progettazione piuttosto che richiedere ai cerchi di eseguire questo comportamento comune.


6

Come @jk. alludendo, il curry può aiutare a rendere il codice più generale.

Ad esempio, supponiamo di avere queste tre funzioni (in Haskell):

> let q a b = (2 + a) * b

> let r g = g 3

> let f a b = b (a 1)

La funzione fqui assume due funzioni come argomenti, passa 1alla prima funzione e passa il risultato della prima chiamata alla seconda funzione.

Se dovessimo chiamare fusando qe rcome argomenti, effettivamente farebbe:

> r (q 1)

dove qverrebbe applicato 1e restituire un'altra funzione (come qè curry); questa funzione restituita verrebbe quindi passata a rcome argomento a cui dare un argomento 3. Il risultato di questo sarebbe un valore di 9.

Ora, supponiamo che avessimo altre due funzioni:

> let s a = 3 * a

> let t a = 4 + a

potremmo passare anche a questi fe ottenere un valore 7o 15, a seconda che i nostri argomenti fossero s to t s. Poiché entrambe le funzioni restituiscono un valore anziché una funzione, nessuna applicazione parziale verrebbe eseguita in f s to f t s.

Se avessimo scritto fcon qe rin mente avremmo potuto usare una lambda (funzione anonima) invece di un'applicazione parziale, ad esempio:

> let f' a b = b (\x -> a 1 x)

ma questo avrebbe limitato la generalità di f'. fpuò essere chiamato con argomenti qe ro se t, ma f'può essere chiamato solo con qe r- f' s ted f' t sentrambi comportano un errore.

DI PIÙ

Se f'fossero chiamati con una q'/ r'coppia in cui il q'prendesse più di due argomenti, q'finirebbe comunque per essere parzialmente applicato f'.

In alternativa, potresti avvolgerti qall'esterno finvece che all'interno, ma ciò ti lascerebbe con una brutta lambda nidificata:

f (\x -> (\y -> q x y)) r

che è essenzialmente ciò che il curry qera in primo luogo!


Mi hai aperto gli occhi. La tua risposta mi ha fatto capire come le funzioni curry / parzialmente applicate sono diverse dalla creazione di una nuova funzione che passa gli argomenti alla funzione originale. 1. Il passaggio di funzioni curryied / paed (come f (q.curry (2)) è più ordinato rispetto alla creazione di funzioni separate inutilmente solo per un uso temporaneo. (In linguaggi funzionali come groovy)
Vigneshwaran,

2. Nella mia domanda, ho detto: "Posso fare lo stesso in C." Sì, ma in linguaggi non funzionali, dove non è possibile passare funzioni come dati, la creazione di una funzione separata, che inoltra i parametri all'originale, non ha tutti i vantaggi di curry / pa
Vigneshwaran

Ho notato che Groovy non supporta il tipo di generalizzazione supportato da Haskell. Ho dovuto scrivere def f = { a, b -> b a.curry(1) }per far f q, rfunzionare e def f = { a, b -> b a(1) }o def f = { a, b -> b a.curry(1)() }per f s, tlavorare. Devi passare tutti i parametri o dire esplicitamente che stai curry. :(
Vigneshwaran,

2
@Vigneshwaran: Sì, è sicuro di dire che Haskell e curry vanno molto bene insieme . ;] Nota che in Haskell, le funzioni sono curry (nella definizione corretta) per impostazione predefinita e gli spazi bianchi indicano l'applicazione della funzione, quindi f x ysignifica che molte lingue scriverebbero f(x)(y), no f(x, y). Forse il tuo codice funzionerebbe in Groovy se scrivi in qmodo da aspettarti di essere chiamato come q(1)(2)?
CA McCann,

1
@Vigneshwaran Sono contento di aver potuto aiutare! Sento il tuo dolore nel dover dire esplicitamente che stai facendo un'applicazione parziale. In Clojure devo fare (partial f a b ...)- essendo abituato a Haskell, mi manca molto il curry durante la programmazione in altre lingue (anche se ho lavorato di recente in F # che, per fortuna, lo supporta).
paul

3

Esistono due punti chiave sull'applicazione parziale. Il primo è sintattico / pratico: alcune definizioni diventano più facili e brevi da leggere e scrivere, come menzionato da @jk. ( Dai un'occhiata alla programmazione Pointfree per saperne di più su quanto sia fantastico!)

Il secondo, come menzionato da @telastyn, riguarda un modello di funzioni e non è semplicemente conveniente. Nella versione di Haskell, da cui trarrò i miei esempi perché non ho familiarità con altre lingue con un'applicazione parziale, tutte le funzioni accettano un singolo argomento. Sì, anche funzioni come:

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

prendere un singolo argomento; a causa dell'associatività del costruttore del tipo di funzione ->, quanto sopra equivale a:

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

che è una funzione che accetta ae restituisce una funzione [a] -> [a].

Questo ci consente di scrivere funzioni come:

($) :: (a -> b) -> a -> b

che può applicare qualsiasi funzione a un argomento del tipo appropriato. Anche quelli pazzi come:

f :: (t, t1) -> t -> t1 -> (t2 -> t3 -> (t, t1)) -> t2 -> t3 -> [(t, t1)]
f q r s t u v = q : (r, s) : [t u v]

f' :: () -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)]
f' = f $ ((), 'a')  -- <== works fine

Va bene, quindi è stato un esempio inventato. Ma una più utile riguarda la classe di tipo Applicativo , che include questo metodo:

(<*>) :: Applicative f => f (a -> b) -> f a -> f b

Come puoi vedere, il tipo è identico a $se togli il Applicative fbit e, in effetti, questa classe descrive l'applicazione di funzioni in un contesto. Quindi, invece della normale applicazione delle funzioni:

ghci> map (+3) [1..5]  
[4,5,6,7,8]

Possiamo applicare funzioni in un contesto applicativo; ad esempio, nel contesto Forse in cui qualcosa può essere presente o mancante:

ghci> Just map <*> Just (+3) <*> Just [1..5]
Just [4,5,6,7,8]

ghci> Just map <*> Nothing <*> Just [1..5]
Nothing

Ora la parte davvero interessante è che la classe di tipo Applicativo non menziona nulla sulle funzioni di più di un argomento - tuttavia, può gestirle, anche funzioni di 6 argomenti come f:

fA' :: Maybe (() -> Char -> (t2 -> t3 -> ((), Char)) -> t2 -> t3 -> [((), Char)])
fA' = Just f <*> Just ((), 'a')

Per quanto ne so, la classe di tipo Applicativo nella sua forma generale non sarebbe possibile senza una concezione dell'applicazione parziale. (Per gli esperti di programmazione là fuori - per favore correggetemi se sbaglio!) Naturalmente, se la lingua manca di applicazione parziale, si potrebbe costruire in in qualche forma, ma ... non è lo stesso, è ? :)


1
Applicativesenza curry o l'applicazione parziale avrebbe usato fzip :: (f a, f b) -> f (a, b). In una lingua con funzioni di ordine superiore, ciò consente di aumentare il curry e l'applicazione parziale nel contesto del funzione ed è equivalente a (<*>). Senza le funzioni di ordine superiore non avrai fmapquindi tutto sarebbe inutile.
CA McCann,

@CAMcCann grazie per il feedback! Sapevo di essere sopra la mia testa con questa risposta. Quindi, quello che ho detto è sbagliato?

1
È corretto nello spirito, certamente. Dividere i peli sulle definizioni di "forma generale", "possibile" e avere una "concezione di applicazione parziale" non cambierà il semplice fatto che l'affascinante f <$> x <*> ystile idiomatico funziona facilmente perché il curry e l'applicazione parziale funzionano facilmente. In altre parole, ciò che è piacevole è più importante di ciò che è possibile qui.
CA McCann,

Ogni volta che vedo esempi di codice di programmazione funzionale, sono più convinto che sia uno scherzo elaborato e che non esista.
Kieveli,

1
@Kieveli è un peccato che ti senta così. Esistono molti ottimi tutorial che ti faranno funzionare con una buona conoscenza delle basi.
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.