Cosa c'è di speciale nel currying 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 currying o nell'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 quale sia la differenza tra tripler1 e tripler2 . Non sono entrambi uguali? Il 'currying' è supportato in linguaggi funzionali puri o parziali come Groovy, Scala, Haskell ecc. Ma posso fare la stessa cosa (left-curry, right-curry, 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 dei posti in cui posso usare il curry e l'applicazione parziale nella mia applicazione Grails, ma esita a farlo perché mi sto chiedendo "Com'è così diverso?"

Per favore mi illumini.

EDIT: Ragazzi, state dicendo che un'applicazione parziale / curring è semplicemente più efficiente della creazione / chiamata di un'altra funzione che inoltra i parametri di default alla funzione originale?

    
posta Vigneshwaran 27.11.2012 - 13:17
fonte

4 risposte

8

Il Currying consiste nel girare / rappresentare una funzione che prende n input in n funzioni che assumono ciascuna 1 input. L'applicazione parziale riguarda la correzione di alcuni degli input a una funzione.

La motivazione per l'applicazione parziale è principalmente che rende più facile scrivere librerie di funzioni di ordine superiore. Ad esempio gli algoritmi in C ++ STL ricorrono in gran parte a predicati o funzioni unarie, bind1st consente all'utente della libreria di agganciare funzioni non unarie con un valore associato. Lo scrittore di librerie non ha bisogno di fornire funzioni sovraccariche per tutti gli algoritmi che accettano funzioni unarie per fornire versioni binarie

Il riciclo di se stesso è utile perché ti dà un'applicazione parziale ovunque tu lo desideri gratuitamente, quindi non hai più bisogno di una funzione come bind1st da applicare parzialmente.

    
risposta data 27.11.2012 - 14:22
fonte
6

But I can do the same thing (left-curry, right-curry, n-curry or partial application) by simply creating another named or anonymous function or closure that will forward the parameters to the original function (like tripler2) in most languages (even C.)

E l'ottimizzatore lo guarderà e proverà prontamente a qualcosa che può capire. Il currying è un piccolo trucco per l'utente finale, ma ha benefici molto migliori dal punto di vista della progettazione linguistica. È veramente bello gestire tutti i metodi come unary A -> B dove B può essere un altro metodo.

Semplifica i metodi che devi scrivere per gestire le funzioni di ordine superiore. L'analisi statica e l'ottimizzazione nella lingua hanno solo un percorso per lavorare con quello che si comporta in modo noto. Il binding dei parametri cade semplicemente fuori dal design piuttosto che richiedere dei cerchi per fare questo comportamento comune.

    
risposta data 27.11.2012 - 15:35
fonte
5

Come @jk. alluso a, currying 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 f qui prende due funzioni come argomenti, passa 1 alla prima funzione e passa il risultato della prima chiamata alla seconda funzione.

Se dovessimo chiamare f usando q e r come argomenti, farebbe in modo efficace:

> r (q 1)

dove q verrebbe applicato a 1 e restituirà un'altra funzione (dato che q è curried); questa funzione restituita sarebbe quindi passata a r come argomento per avere un argomento di 3 . Il risultato di questo sarebbe un valore di 9 .

Ora, diciamo che avevamo altre due funzioni:

> let s a = 3 * a

> let t a = 4 + a

potremmo trasferirli anche a f e ottenere un valore di 7 o 15 , a seconda che i nostri argomenti fossero s t o t s . Poiché queste funzioni restituiscono entrambi un valore anziché una funzione, nessuna applicazione parziale avverrà in f s t o f t s .

Se avessimo scritto f con q e r , potremmo aver usato una lambda (funzione anonima) invece di un'applicazione parziale, ad esempio:

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

ma ciò avrebbe limitato la generalità di f' . f può essere chiamato con argomenti q e r o s e t , ma f' può essere chiamato solo con q e r - f' s t e f' t s entrambi genera un errore.

Altro

Se f' è stato chiamato con una coppia q' / r' in cui q' ha richiesto più di due argomenti, il q' verrebbe comunque parzialmente applicato in f' .

In alternativa, potresti inserire q al di fuori di f anziché all'interno, ma questo ti lascerebbe con un brutto lambda annidato:

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

che è essenzialmente ciò che il% al% alesato al curry era al primo posto!

    
risposta data 29.11.2012 - 21:01
fonte
3

Ci sono due punti chiave sull'applicazione parziale. Il primo è sintattico / pratico: alcune definizioni diventano più semplici e più brevi da leggere e scrivere, come menzionato da @jk. (Consulta Programmazione senza punti per ulteriori informazioni su quanto è fantastico!)

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

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

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

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

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

Questo ci permette di scrivere funzioni come:

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

che può applicare la funzione qualsiasi a un argomento del tipo appropriato. Anche 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

Ok, quindi è stato un esempio forzato. Ma uno più utile riguarda la classe Applicative , che include questo metodo:

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

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

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

Possiamo applicare le funzioni in un contesto Applicativo; ad esempio, nel contesto di Maybe 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 veramente interessante è che la classe di tipo Applicative non menziona qualsiasi 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 certa concezione dell'applicazione parziale. (Per eventuali esperti di programmazione là fuori - per favore correggimi se sbaglio!) Naturalmente, se la tua lingua manca di un'applicazione parziale, potresti costruirla in qualche modo, ma ... è non è lo stesso, vero? :)

    
risposta data 30.11.2012 - 15:59
fonte