Co jest specjalnego w curry lub częściowej aplikacji?

9

Codziennie czytam artykuły na temat programowania funkcjonalnego i staram się stosować jak najwięcej praktyk. Ale nie rozumiem, co jest wyjątkowego w curry lub częściowym stosowaniu.

Weź ten kod Groovy jako przykład:

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

Nie rozumiem, jaka jest różnica między tripler1i tripler2. Czy oba nie są takie same? „Curry” jest obsługiwane w czystych lub częściowych językach funkcjonalnych, takich jak Groovy, Scala, Haskell itp. Ale mogę zrobić to samo (lewe curry, prawe curry, n-curry lub częściowe zastosowanie), po prostu tworząc inny nazwany lub anonimowy funkcja lub zamknięcie, które przekaże parametry do pierwotnej funkcji (jak tripler2) w większości języków (nawet C.)

Czy coś mi umyka? Są miejsca, w których mogę używać curry i częściowej aplikacji w mojej aplikacji Grails, ale waham się, aby to zrobić, ponieważ zadaję sobie pytanie „Jak to inaczej?”

Proszę, oświeć mnie.

EDYCJA: Czy mówicie, że częściowa aplikacja / curry jest po prostu bardziej wydajna niż tworzenie / wywoływanie innej funkcji, która przekazuje domyślne parametry do pierwotnej funkcji?

Vigneshwaran
źródło
1
Czy ktoś może utworzyć tagi „curry” lub „curry”?
Vigneshwaran
Jak się curry w C?
Giorgio
jest to prawdopodobnie naprawdę więcej o programistach
. stackexchange.com/questions/152868/…
1
@Vigneshwaran: AFAIK, nie musisz tworzyć innej funkcji w języku obsługującym curry. Na przykład w języku Haskell f x y = x + yoznacza, że fjest to funkcja, która przyjmuje jeden parametr int. Wynik f x( fzastosowany do x) jest funkcją, która przyjmuje jeden parametr int. Wynik f x y(lub (f x) ynp. f xZastosowany y) jest wyrażeniem, które nie przyjmuje parametrów wejściowych i jest oceniane przez redukcję x + y.
Giorgio
1
Możesz osiągnąć te same rzeczy, ale wysiłek, jaki wykonujesz z C jest o wiele bardziej bolesny i nie tak skuteczny, jak w języku takim jak haskell, w którym jest to domyślne zachowanie
Daniel Gratzer

Odpowiedzi:

8

Curry polega na zamianie / reprezentowaniu funkcji, która pobiera n danych wejściowych do n funkcji, z których każda przyjmuje 1 sygnał wejściowy. Częściowe zastosowanie polega na ustaleniu niektórych danych wejściowych funkcji.

Motywacja do częściowego zastosowania polega przede wszystkim na tym, że ułatwia pisanie bibliotek funkcji wyższego rzędu. Na przykład wszystkie algorytmy w C ++ STL w większości przyjmują predykaty lub funkcje jednoargumentowe, bind1st pozwala użytkownikowi biblioteki na dołączenie niejednorodnych funkcji ze związaną wartością. Piszący bibliotekę nie musi więc zapewniać przeciążonych funkcji dla wszystkich algorytmów, które przyjmują jednoargumentowe funkcje w celu zapewnienia wersji binarnych

Samo curry jest użyteczne, ponieważ daje ci częściową aplikację w dowolnym miejscu za darmo, tzn. Nie potrzebujesz już funkcji takiej jak bind1stczęściowe zastosowanie.

jk.
źródło
czy jest curryingcoś specyficznego dla groovy lub ma zastosowanie we wszystkich językach?
amfibia
@foampile jest to coś, co ma zastosowanie we wszystkich językach, ale jak na ironię groovy curry tak naprawdę nie robi tego programiści.stackexchange.com/
jk.
@jk. Czy mówisz, że curry / częściowa aplikacja jest bardziej wydajna niż tworzenie i wywoływanie innej funkcji?
Vigneshwaran,
2
@Vigneshwaran - niekoniecznie jest bardziej wydajny, ale zdecydowanie bardziej wydajny pod względem czasu programisty. Należy również pamiętać, że chociaż curry jest obsługiwane przez wiele języków funkcjonalnych, ale na ogół nie jest obsługiwane w OO lub językach proceduralnych. (A przynajmniej nie przez sam język.)
Stephen C
6

Ale mogę zrobić to samo (lewe curry, prawe curry, n-curry lub aplikacja częściowa), po prostu tworząc inną nazwaną lub anonimową funkcję lub zamknięcie, które przekaże parametry do pierwotnej funkcji (np. Tripler2) w większości języków ( nawet C.)

Optymalizator przyjrzy się temu i natychmiast przejdzie do czegoś, co może zrozumieć. Curry to niezła sztuczka dla użytkownika końcowego, ale przynosi znacznie lepsze korzyści z punktu widzenia projektowania języka. To naprawdę miło obsługiwać wszystkie metody jako jednoskładnikowa, A -> Bgdzie Bmoże być inna metoda.

Upraszcza to, jakie metody musisz pisać, aby obsługiwać funkcje wyższego rzędu. Twoja statyczna analiza i optymalizacja w języku ma tylko jedną ścieżkę do pracy, która zachowuje się w znany sposób. Wiązanie parametrów po prostu wypada z projektu, zamiast wymagać od obręczy wykonywania tego typowego zachowania.

Telastyn
źródło
6

Jako @jk. Nawiązując do curry, może pomóc uczynić kod bardziej ogólnym.

Załóżmy na przykład, że masz te trzy funkcje (w Haskell):

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

> let r g = g 3

> let f a b = b (a 1)

Funkcja fprzyjmuje tutaj dwie funkcje jako argumenty, przekazuje 1do pierwszej funkcji i przekazuje wynik pierwszego wywołania do drugiej funkcji.

Gdybyśmy mieli zadzwonić fza pomocą qi rjako argumentów, efektywnie by to robił:

> r (q 1)

gdzie qzostanie zastosowane 1i zwróci inną funkcję (jak qcurry); ta zwrócona funkcja byłaby następnie przekazana rjako argument do podania argumentu 3. Wynikiem tego będzie wartość 9.

Powiedzmy, że mieliśmy dwie inne funkcje:

> let s a = 3 * a

> let t a = 4 + a

moglibyśmy je frównież przekazać i uzyskać wartość 7lub 15, w zależności od tego, czy nasze argumenty były s tlub t s. Ponieważ obie te funkcje zwracają wartość, a nie funkcję, żadna częściowa aplikacja nie miałaby miejsca w f s tlub f t s.

Gdybyśmy pisali fz myślą qi rmyślą, moglibyśmy użyć lambda (funkcja anonimowa) zamiast częściowej aplikacji, np .:

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

ale to ograniczyłoby ogólność f'. fmożna wywoływać za pomocą argumentów qi rlub si t, ale f'można je wywoływać tylko za pomocą qi r- f' s ti f' t soba powodują błąd.

WIĘCEJ

Gdyby f'zostały wywołane za pomocą pary q'/, w r'której q'wzięto więcej niż dwa argumenty, q'nadal byłby częściowo zastosowany f'.

Alternatywnie możesz owinąć się na qzewnątrz fzamiast wewnątrz, ale to dałoby ci paskudną zagnieżdżoną lambdę:

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

co w gruncie rzeczy qbyło curry !

Paweł
źródło
Otworzyłeś mi oczy Twoja odpowiedź uświadomiła mi, czym różnią się funkcje curry / częściowo zastosowane od utworzenia nowej funkcji, która przekazuje argumenty do funkcji oryginalnej. 1. Przekazywanie funkcji curryied / paed (takich jak f (q.curry (2)) jest przyjemniejsze niż niepotrzebne tworzenie oddzielnych funkcji tylko do chwilowego użytku. (W funkcjonalnych językach, takich jak groovy)
Vigneshwaran
2. W moim pytaniu powiedziałem: „Mogę zrobić to samo w C.” Tak, ale w językach niefunkcjonalnych, w których nie można przekazywać funkcji jako danych, tworzenie osobnej funkcji, która przekazuje parametry do oryginału, nie ma wszystkich zalet
curry
Zauważyłem, że Groovy nie obsługuje tego rodzaju uogólnień, jakie obsługuje Haskell. Miałem napisać def f = { a, b -> b a.curry(1) }, aby f q, rdo pracy i def f = { a, b -> b a(1) }czy def f = { a, b -> b a.curry(1)() }w f s, tdo pracy. Musisz przekazać wszystkie parametry lub wyraźnie powiedzieć, że curry. :(
Vigneshwaran
2
@Vigneshwaran: Tak, można śmiało powiedzieć, że Haskell i curry bardzo dobrze do siebie pasują . ;] Zauważ, że w Haskell funkcje są domyślnie curry (w poprawnej definicji), a białe znaki oznaczają zastosowanie funkcji, więc f x yoznacza to, ile języków napisałby f(x)(y), a nie f(x, y). Być może twój kod działałby w Groovy, jeśli piszesz qtak, że spodziewa się, że zostanie wywołany jak q(1)(2)?
CA McCann
1
@Vigneshwaran Cieszę się, że mogłem pomóc! Czuję twój ból z powodu konieczności wyraźnego powiedzenia, że ​​dokonujesz częściowego zastosowania. W Clojure muszę to zrobić (partial f a b ...)- będąc przyzwyczajonym do Haskella, bardzo brakuje mi właściwego curry podczas programowania w innych językach (chociaż ostatnio pracuję w F #, który, na szczęście, obsługuje).
Paul
3

Istnieją dwa kluczowe punkty dotyczące częściowego zastosowania. Pierwsza to składnia / wygoda - niektóre definicje stają się łatwiejsze i krótsze do czytania i pisania, jak wspomniano w @jk. (Sprawdź programowanie Pointfree, aby dowiedzieć się więcej o tym, jak niesamowite jest to!)

Drugi, jak wspomniano @telastyn, dotyczy modelu funkcji i nie jest jedynie wygodny. W wersji Haskell, z której otrzymam moje przykłady, ponieważ nie znam innych języków z częściową aplikacją, wszystkie funkcje pobierają pojedynczy argument. Tak, nawet takie funkcje jak:

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

weź jeden argument; z powodu asocjatywności konstruktora typu funkcji ->powyższe jest równoważne z:

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

która jest funkcją, która pobiera ai zwraca funkcję [a] -> [a].

To pozwala nam pisać funkcje takie jak:

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

który może zastosować dowolną funkcję do argumentu odpowiedniego typu. Nawet szalone, takie jak:

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

Okej, to był wymyślony przykład. Ale bardziej przydatna jest klasa typu Applicative , która obejmuje tę metodę:

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

Jak widać, typ jest identyczny jak w $przypadku pobrania Applicative fbitu, a w rzeczywistości ta klasa opisuje zastosowanie funkcji w kontekście. Więc zamiast normalnej aplikacji funkcji:

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

Możemy zastosować funkcje w kontekście aplikacyjnym; na przykład w kontekście Może, w którym coś może być obecne lub brakujące:

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

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

Teraz naprawdę fajne jest to, że klasa typu Applicative nie wspomina nic o funkcjach więcej niż jednego argumentu - niemniej może sobie z nimi poradzić, nawet funkcje 6 argumentów takich jak f:

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

O ile mi wiadomo, klasa typu Wnioskodawca w swojej ogólnej formie nie byłaby możliwa bez pewnej koncepcji częściowego zastosowania. (Do dowolnego programowania ekspertów tam - proszę mnie poprawić, jeśli się mylę!) Oczywiście, jeśli język brakuje częściową aplikacji, mogłaby zbudować go w jakiejś formie, ale ... to nie jest tak samo, to jest ? :)


źródło
1
Applicativebez curry lub częściowego zastosowania fzip :: (f a, f b) -> f (a, b). W języku z funkcjami wyższego rzędu pozwala to przenieść curry i częściowe zastosowanie do kontekstu funktora i jest równoważne z (<*>). Bez funkcji wyższego rzędu nie będziesz mieć, fmapwięc wszystko byłoby bezużyteczne.
CA McCann
@CAMcCann dzięki za opinie! Wiedziałem, że jestem gotów nad tą odpowiedzią. Więc co powiedziałem źle?
1
Z pewnością jest to zgodne z duchem. Podział włosów na definicje „formy ogólnej”, „możliwej” i „koncepcja częściowej aplikacji” nie zmieni prostego faktu, że czarujący f <$> x <*> yidiomatyczny styl działa łatwo, ponieważ curry i częściowa aplikacja łatwo działają. Innymi słowy, to, co przyjemne, jest ważniejsze niż to, co jest możliwe tutaj.
CA McCann
Za każdym razem, gdy widzę przykłady kodu programowania funkcjonalnego, jestem bardziej przekonany, że to skomplikowany żart i że nie istnieje.
Kieveli,
1
@Kieveli to niefortunne, że tak się czujesz. Istnieje wiele świetnych samouczków , które pomogą Ci rozpocząć pracę z dobrym zrozumieniem podstaw.