Jaka jest zaleta curry?

154

Właśnie dowiedziałem się o curry i chociaż myślę, że rozumiem tę koncepcję, nie widzę żadnej dużej korzyści z jej używania.

Jako trywialny przykład używam funkcji, która dodaje dwie wartości (zapisane w ML). Wersja bez curry byłaby

fun add(x, y) = x + y

i będzie nazywany jako

add(3, 5)

podczas gdy wersja curry jest

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

i będzie nazywany jako

add 3 5

Wydaje mi się, że to po prostu cukier składniowy, który usuwa jeden zestaw nawiasów z definiowania i wywoływania funkcji. Widziałem curry wymienione jako jedną z ważnych cech funkcjonalnych języków i jestem w tej chwili nieco rozczarowany. Koncepcja utworzenia łańcucha funkcji, które wykorzystują każdy pojedynczy parametr, zamiast funkcji, która wymaga krotki, wydaje się dość skomplikowana w użyciu do prostej zmiany składni.

Czy nieco prostsza składnia jest jedyną motywacją do curry, czy też brakuje mi innych zalet, które nie są oczywiste w moim bardzo prostym przykładzie? Czy curry to tylko cukier składniowy?

Szalony naukowiec
źródło
54
Samo curry jest zasadniczo bezużyteczne, ale domyślnie wszystkie funkcje curry sprawiają, że wiele innych funkcji jest znacznie przyjemniejszych w użyciu. Trudno to docenić, dopóki faktycznie nie użyjesz funkcjonalnego języka.
CA McCann
4
Coś, o czym wspomniałem przechodząc przez delnana w komentarzu do odpowiedzi JoelEtherton, ale myślałem, że wspomnę wyraźnie, że (przynajmniej w Haskell) można częściowo zastosować nie tylko funkcje, ale także konstruktory typów - może to być całkiem poręczny; to może być coś do przemyślenia.
Paweł
Wszyscy podali przykłady Haskella. Można się dziwić, że curry jest przydatne tylko w Haskell.
Manoj R
@ManojR Wszyscy nie podali przykładów w Haskell.
phwd
1
Pytanie wywołało dość interesującą dyskusję na temat Reddit .
yannis

Odpowiedzi:

126

Dzięki funkcjom curry łatwiejsze jest korzystanie z bardziej abstrakcyjnych funkcji, ponieważ można się specjalizować. Powiedzmy, że masz funkcję dodawania

add x y = x + y

i że chcesz dodać 2 do każdego członka listy. W Haskell zrobiłbyś to:

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

Tutaj składnia jest lżejsza niż w przypadku konieczności utworzenia funkcji add2

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

lub jeśli musiałbyś wykonać anonimową funkcję lambda:

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

Pozwala także na abstrahowanie od różnych implementacji. Powiedzmy, że masz dwie funkcje wyszukiwania. Jedna z listy par klucz / wartość i klucz do wartości, a druga z mapy od kluczy do wartości i klucza do wartości, jak poniżej:

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

Następnie możesz utworzyć funkcję, która zaakceptowała funkcję wyszukiwania od klucza do wartości. Możesz przekazać dowolną z powyższych funkcji wyszukiwania, częściowo stosowaną odpowiednio z listą lub mapą:

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

Podsumowując: curry jest dobre, ponieważ pozwala na specjalizację / częściowe zastosowanie funkcji przy użyciu lekkiej składni, a następnie przekazanie tych częściowo zastosowanych funkcji do funkcji wyższego rzędu, takich jak maplub filter. Funkcje wyższego rzędu (które przyjmują funkcje jako parametry lub dają je jako wyniki) są chlebem powszednim programowania funkcjonalnego, a funkcje curry i częściowo zastosowane pozwalają na korzystanie z funkcji wyższego rzędu o wiele bardziej skutecznie i zwięźle.

Boris
źródło
31
Warto zauważyć, że z tego powodu porządek argumentów używany dla funkcji w Haskell często opiera się na prawdopodobieństwie częściowego zastosowania, co z kolei powoduje, że opisane powyżej korzyści mają zastosowanie (ha, ha) w większej liczbie sytuacji. W ten sposób domyślnie currymowanie jest nawet bardziej korzystne niż wynika to z konkretnych przykładów, takich jak te tutaj.
CA McCann
wat. „Jedna z listy par klucz / wartość i klucz do wartości, a druga z mapy od kluczy do wartości i klucza do wartości”
Mateen Ulhaq
@MateenUlhaq Jest to kontynuacja poprzedniego zdania, w którym przypuszczam, że chcemy uzyskać wartość opartą na kluczu i mamy na to dwa sposoby. Zdanie wylicza te dwa sposoby. W pierwszej kolejności otrzymujesz listę par klucz / wartość oraz klucz, dla którego chcemy znaleźć wartość, a w inny sposób otrzymujemy odpowiednią mapę i ponownie klucz. Pomocne może być spojrzenie na kod bezpośrednio po zdaniu.
Boris
53

Praktyczną odpowiedzią jest to, że curry znacznie ułatwia tworzenie anonimowych funkcji. Nawet przy minimalnej składni lambda jest to coś w rodzaju wygranej; porównać:

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

Jeśli masz brzydką składnię lambda, jest jeszcze gorzej. (Patrzę na ciebie, JavaScript, Scheme i Python).

Staje się to coraz bardziej przydatne, gdy korzystasz z coraz większej liczby funkcji wyższego rzędu. Chociaż w Haskell używam więcej funkcji wyższego rzędu niż w innych językach, odkryłem, że faktycznie używam mniejszej składni lambda, ponieważ mniej więcej dwie trzecie czasu, lambda byłaby tylko częściowo zastosowaną funkcją. (I przez większość czasu wyodrębniam go do nazwanej funkcji.)

Zasadniczo nie zawsze jest oczywiste, która wersja funkcji jest „kanoniczna”. Na przykład weź map. Typ mapmożna zapisać na dwa sposoby:

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

Który z nich jest „poprawny”? Trudno powiedzieć. W praktyce większość języków korzysta z pierwszego - mapa przyjmuje funkcję i listę i zwraca listę. Jednak zasadniczo to, co faktycznie robi mapa, to mapowanie normalnych funkcji na listę funkcji - bierze funkcję i zwraca funkcję. Jeśli mapa jest curry, nie musisz odpowiadać na to pytanie: robi obie rzeczy w bardzo elegancki sposób.

Staje się to szczególnie ważne po uogólnieniu mapna typy inne niż lista.

Ponadto curry naprawdę nie jest bardzo skomplikowane. To trochę uproszczenie w stosunku do modelu używanego przez większość języków: nie potrzebujesz żadnego pojęcia funkcji wielu argumentów upieczonych w twoim języku. Odzwierciedla to również dokładniej rachunek lambda.

Oczywiście, języki w stylu ML nie mają pojęcia wielu argumentów w formie curry lub niesprzecznej. f(a, b, c)Składnia faktycznie odpowiada przejściu w krotce (a, b, c)w f, więc fnadal trwa tylko na argument. Jest to w rzeczywistości bardzo przydatne rozróżnienie, które chciałbym mieć w innych językach, ponieważ sprawia, że ​​pisanie czegoś takiego jest bardzo naturalne:

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

Nie można tego łatwo zrobić w przypadku języków, w których od razu pojawia się pomysł na wiele argumentów!

Tikhon Jelvis
źródło
1
„Języki w stylu ML nie mają pojęcia wielu argumentów w formie curry lub w formie bezsurowej”: pod tym względem, czy styl Haskell ML?
Giorgio
1
@Giorgio: Tak.
Tikhon Jelvis
1
Ciekawy. Znam trochę Haskella i uczę się teraz języka SML, więc interesujące są różnice i podobieństwa między tymi dwoma językami.
Giorgio
Świetna odpowiedź, a jeśli nadal nie jesteś przekonany, pomyśl o potokach uniksowych, które są podobne do strumieni lambda
Sridhar Sarnobat,
„Praktyczna” odpowiedź nie ma większego znaczenia, ponieważ gadatliwości zwykle unika się przez częściowe zastosowanie , a nie curry. I argumentowałbym tutaj, że składnia abstrakcji lambda (pomimo deklaracji typu) jest brzydsza niż ta (przynajmniej) w Schemacie, ponieważ wymaga ona więcej wbudowanych specjalnych reguł składniowych, aby poprawnie parsować ją, co powoduje, że specyfikacja języka jest bezużyteczna o właściwościach semantycznych.
FrankHB,
24

Curry może być przydatne, jeśli masz funkcję, którą przekazujesz jako obiekt pierwszej klasy i nie otrzymujesz wszystkich parametrów potrzebnych do oceny tego w jednym miejscu w kodzie. Możesz po prostu zastosować jeden lub więcej parametrów po ich otrzymaniu i przekazać wynik do innego fragmentu kodu, który ma więcej parametrów i dokończyć jego ocenę.

Kod do osiągnięcia tego celu będzie prostszy niż wtedy, gdy trzeba najpierw zebrać wszystkie parametry.

Istnieje również możliwość ponownego wykorzystania kodu, ponieważ funkcje przyjmujące pojedynczy parametr (kolejna funkcja curry) nie muszą być tak dokładnie dopasowane do wszystkich parametrów.

psr
źródło
14

Główna motywacja (przynajmniej początkowo) do curry była niepraktyczna, ale teoretyczna. W szczególności curry umożliwia efektywne uzyskiwanie funkcji wielu argumentów bez faktycznego definiowania semantyki dla nich lub definiowania semantyki dla produktów. Prowadzi to do prostszego języka z taką samą ekspresją jak inny, bardziej skomplikowany język, i dlatego jest pożądany.

Alex R.
źródło
2
Chociaż motywacja jest tutaj teoretyczna, myślę, że prostota jest prawie zawsze praktyczną zaletą. Nie przejmowanie się funkcjami wieloparumentowymi ułatwia mi życie, gdy programuję, podobnie jak w przypadku semantyki.
Tikhon Jelvis
2
@ TikhonJelvis Podczas programowania curry daje ci inne rzeczy do zmartwienia, na przykład kompilator nie wychwytuje faktu, że przekazałeś zbyt mało argumentów do funkcji, a nawet pojawia się zły komunikat błędu; gdy nie używasz curry, błąd jest znacznie bardziej widoczny.
Alex R
Nigdy nie miałem takich problemów: przynajmniej GHC jest pod tym względem bardzo dobry. Kompilator zawsze wychwytuje tego rodzaju problem i ma również dobre komunikaty o błędach dla tego błędu.
Tikhon Jelvis
1
Nie mogę się zgodzić, że komunikaty o błędach kwalifikują się jako dobre. Obsługiwalne, tak, ale nie są jeszcze dobre. Łapie również tego rodzaju problemy tylko wtedy, gdy powoduje błąd typu, tj. Jeśli później spróbujesz użyć wyniku jako czegoś innego niż funkcja (lub wpiszesz adnotację, ale poleganie na tym w przypadku błędów czytelnych ma swoje problemy ); zgłoszona lokalizacja błędu jest oddzielana od jego rzeczywistej lokalizacji.
Alex R
14

(Podam przykłady w Haskell.)

  1. Podczas używania języków funkcjonalnych bardzo wygodnie jest częściowo zastosować funkcję. Podobnie jak u Haskella (== x)jest funkcja, która zwraca, Truejeśli jej argument jest równy podanemu terminowi x:

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

    bez curry, mielibyśmy nieco mniej czytelny kod:

    mem x lst = any (\y -> y == x) lst
    
  2. Jest to związane z programowaniem Tacit (patrz także styl Pointfree na wiki Haskell). Ten styl koncentruje się nie na wartościach reprezentowanych przez zmienne, ale na komponowaniu funkcji i sposobie przepływu informacji przez łańcuch funkcji. Możemy przekonwertować nasz przykład na formularz, który w ogóle nie używa zmiennych:

    mem = any . (==)
    

    Tutaj widzimy ==jako funkcję od ado a -> Booli anyjako funkcję od a -> Booldo [a] -> Bool. Po prostu je komponując, otrzymujemy wynik. Wszystko to dzięki curry.

  3. W niektórych sytuacjach przydatne jest również odwracanie, bez curry. Załóżmy na przykład, że chcemy podzielić listę na dwie części - elementy mniejsze niż 10 i pozostałe, a następnie połączyć te dwie listy. Podział listy odbywa się za pomocą (tutaj również używamy curry ). Wynik jest typu . Zamiast wyodrębnić wynik do pierwszej i drugiej części i połączyć je za pomocą , możemy to zrobić bezpośrednio, nie wywołując aspartition (< 10)<([Int],[Int])++++

    uncurry (++) . partition (< 10)
    

Rzeczywiście, (uncurry (++) . partition (< 10)) [4,12,11,1]ocenia na [4,1,12,11].

Istnieją również ważne zalety teoretyczne:

  1. Curry jest niezbędne w przypadku języków, w których brakuje typów danych i które mają tylko funkcje, takie jak rachunek lambda . Chociaż te języki nie są przydatne w praktyce, są bardzo ważne z teoretycznego punktu widzenia.
  2. Jest to związane z istotną właściwością języków funkcjonalnych - funkcje są obiektami pierwszej klasy. Jak widzieliśmy, konwersja z (a, b) -> cna a -> (b -> c)oznacza, że ​​wynik tej ostatniej funkcji jest typu b -> c. Innymi słowy, wynik jest funkcją.
  3. (Un) curry jest ściśle powiązane z kartezjańskimi kategoriami zamkniętymi , co jest kategorycznym sposobem przeglądania kalkulowanych lambda.
Petr Pudlák
źródło
W przypadku „dużo mniej czytelnego kodu”, prawda mem x lst = any (\y -> y == x) lst? (Z odwrotnym ukośnikiem).
stusmith
Tak, dziękuję za zwrócenie na to uwagi. Naprawię to.
Petr Pudlák
9

Curry to nie tylko cukier składniowy!

Rozważ podpisy typu add1(niespieszne) i add2(curry):

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

(W obu przypadkach nawiasy w podpisie są opcjonalne, ale dla jasności zamieściłem je).

add1Jest to funkcja, która bierze 2-krotki inti inti zwraca int. add2jest funkcją, która przyjmuje inti zwraca inną funkcję, która z kolei przyjmuje inti zwraca an int.

Zasadnicza różnica między nimi staje się bardziej widoczna, gdy wyraźnie określimy aplikację funkcji. Zdefiniujmy funkcję (nie curry), która zastosuje swój pierwszy argument do drugiego argumentu:

apply(f, b) = f b

Teraz widzimy różnicę między add1i add2wyraźniej. add1zostaje sprawdzony z 2-krotkami:

apply(add1, (3, 5))

ale add2zostaje wywołany za pomocą an, int a następnie jego wartość zwracana jest wywołana przez innąint :

apply(apply(add2, 3), 5)

EDYCJA: Podstawową zaletą curry jest to, że otrzymujesz częściową aplikację za darmo. Powiedzmy, że chcesz funkcji typu int -> int(powiedzmy, mapnad listą), która dodała 5 do jej parametru. Mógłbyś pisać addFiveToParam x = x+5lub mógłbyś zrobić to samo z wbudowaną lambda, ale możesz także znacznie łatwiej (szczególnie w przypadkach mniej trywialnych niż ten) pisać add2 5!

Płomień Pthariena
źródło
3
Rozumiem, że w moim przykładzie za kulisami istnieje duża różnica, ale wynik wydaje się być prostą zmianą składniową.
Szalony naukowiec
5
Curry nie jest bardzo głęboką koncepcją. Chodzi o uproszczenie modelu bazowego (patrz Rachunek Lambda) lub w językach, które i tak mają krotki, w rzeczywistości chodzi o wygodę syntaktyczną częściowego zastosowania. Nie lekceważ znaczenia wygody syntaktycznej.
Peaker
9

Curry to po prostu cukier składniowy, ale myślę, że trochę nie rozumiesz, co robi cukier. Biorąc swój przykład,

fun add x y = x + y

jest właściwie cukrem syntaktycznym

fun add x = fn y => x + y

Oznacza to, że (dodaj x) zwraca funkcję, która pobiera argument y i dodaje x do y.

fun addTuple (x, y) = x + y

Jest to funkcja, która pobiera krotkę i dodaje jej elementy. Te dwie funkcje są w rzeczywistości dość różne; biorą różne argumenty.

Jeśli chcesz dodać 2 do wszystkich liczb na liście:

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

Wynik byłby [3,4,5].

Z drugiej strony, jeśli chcesz podsumować każdą krotkę na liście, funkcja addTuple idealnie pasuje.

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

Wynik byłby [12,13,14].

Funkcje curry są świetne tam, gdzie przydatne jest częściowe zastosowanie - na przykład mapowanie, składanie, aplikacja, filtrowanie. Rozważ tę funkcję, która zwraca największą liczbę dodatnią z podanej listy lub 0, jeśli nie ma liczb dodatnich:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 
gnud
źródło
1
Zrozumiałem, że funkcja curry ma podpis innego typu i że w rzeczywistości jest to funkcja, która zwraca inną funkcję. Brakowało mi jednak częściowej aplikacji.
Szalony naukowiec
9

Inną rzeczą, o której jeszcze nie wspomniałem, jest to, że curry pozwala (ograniczoną) abstrakcji ponad jałowość.

Rozważ te funkcje, które są częścią biblioteki Haskella

(.) :: (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

W każdym przypadku zmienna typu cmoże być typem funkcji, dzięki czemu funkcje te działają na pewnym prefiksie listy parametrów argumentów. Bez curry będziesz potrzebować specjalnej funkcji językowej, aby wyodrębnić liczbę funkcji lub mieć wiele różnych wersji tych funkcji przeznaczonych dla różnych rodzajów.

Geoff Reedy
źródło
6

Moje ograniczone zrozumienie jest takie:

1) Zastosowanie funkcji częściowej

Aplikacja funkcji częściowej to proces zwracający funkcję, która przyjmuje mniejszą liczbę argumentów. Jeśli podasz 2 z 3 argumentów, funkcja zwróci 3-2 = 1 argument. Jeśli podasz 1 z 3 argumentów, zwróci funkcję, która przyjmuje 3-1 = 2 argumenty. Jeśli chcesz, możesz nawet częściowo zastosować 3 z 3 argumentów, a to zwróci funkcję, która nie przyjmuje argumentu.

Biorąc pod uwagę następującą funkcję:

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

Przy wiązaniu 1 do x i częściowym zastosowaniu tego do powyższej funkcji f(x,y,z)otrzymasz:

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

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

Teraz, jeśli miałbyś przypisać y do 2 i z do 3, a częściowo zastosować f'(y,z), otrzymasz:

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

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

Teraz w dowolnym momencie, można wybrać, aby ocenić f, f'czy f''. Więc mogę zrobić:

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

lub

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

2) Curry

Z drugiej strony curry to proces dzielenia funkcji na zagnieżdżony łańcuch funkcji jednego argumentu. Nigdy nie możesz podać więcej niż jednego argumentu, jest to jeden lub zero.

Biorąc pod uwagę tę samą funkcję:

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

Jeśli to zrobisz, uzyskasz łańcuch 3 funkcji:

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

Gdzie:

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

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

f'''(z) = z;

Teraz, jeśli zadzwonisz f'(x)z x = 1:

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

Otrzymałeś nową funkcję:

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

Jeśli zadzwonisz g(y)z y = 2:

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

Otrzymałeś nową funkcję:

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

Wreszcie, jeśli zadzwonisz h(z)z z = 3:

h(3) = 1 + 2 + 3;

Jesteś zwrócony 6.

3) Zamknięcie

Wreszcie, Zamknięcie to proces przechwytywania funkcji i danych razem jako pojedynczej jednostki. Zamknięcie funkcji może zająć 0 do nieskończonej liczby argumentów, ale jest także świadomy danych, które do niej nie są przekazywane.

Ponownie, biorąc pod uwagę tę samą funkcję:

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

Zamiast tego możesz napisać zamknięcie:

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

Gdzie:

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

f'jest zamknięty x. Oznacza to, że f'można odczytać wartość x, która jest w środku f.

Więc jeśli miałbyś zadzwonić fz x = 1:

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

Dostaniesz zamknięcie:

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

Teraz, jeśli zadzwoniłeś za closureOfFpomocą y = 2i z = 3:

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

Które wróci 6

Wniosek

Curry, częściowe zastosowanie i zamknięcia są nieco podobne, ponieważ rozkładają funkcję na więcej części.

Curry rozkłada funkcję wielu argumentów na zagnieżdżone funkcje pojedynczych argumentów, które zwracają funkcje pojedynczych argumentów. Nie ma sensu currywać funkcji jednego lub mniej argumentu, ponieważ nie ma to sensu.

Częściowa aplikacja rozkłada funkcję wielu argumentów na funkcję mniejszych argumentów, których obecnie brakujących argumentów zastąpiono podaną wartością.

Zamknięcie rozkłada funkcję na funkcję i zestaw danych, w którym zmienne wewnątrz funkcji, które nie zostały przekazane, mogą zaglądać do zestawu danych w celu znalezienia wartości do powiązania, gdy zostanie poproszony o ocenę.

Mylące w tym wszystkim jest to, że mogą one być używane do implementacji podzbioru pozostałych. W gruncie rzeczy wszystkie są trochę szczegółami implementacyjnymi. Wszystkie zapewniają podobną wartość, ponieważ nie trzeba zbierać wszystkich wartości z góry, i że można ponownie użyć części funkcji, ponieważ rozłożyłeś ją na dyskretne jednostki.

Ujawnienie

W żadnym wypadku nie jestem ekspertem w tej dziedzinie, dopiero niedawno zacząłem się o nich uczyć, dlatego zapewniam moje obecne zrozumienie, ale mogą zawierać błędy, na które chciałbym zwrócić uwagę, i poprawię je, jeśli: Odkryłem każdy.

Didier A.
źródło
1
Odpowiedź brzmi: curry nie ma przewagi?
ceving,
1
@ceving O ile mi wiadomo, to prawda. W praktyce curry i częściowe stosowanie daje te same korzyści. Wybór, który należy wdrożyć w języku, jest dokonywany ze względów implementacyjnych, jeden może być łatwiejszy do wdrożenia niż inny, biorąc pod uwagę określony język.
Didier A.,
5

Curry (częściowa aplikacja) pozwala utworzyć nową funkcję z istniejącej funkcji poprzez ustalenie niektórych parametrów. Jest to szczególny przypadek zamknięcia leksykalnego, w którym funkcja anonimowa jest tylko trywialnym opakowaniem, które przekazuje niektóre przechwycone argumenty do innej funkcji. Możemy to również zrobić, stosując ogólną składnię do wykonywania zamknięć leksykalnych, ale częściowe zastosowanie zapewnia uproszczony cukier składniowy.

Dlatego programiści Lisp, pracując w stylu funkcjonalnym, czasami używają bibliotek do częściowej aplikacji .

Zamiast (lambda (x) (+ 3 x)), co daje nam funkcję, która dodaje 3 do swojego argumentu, możesz napisać coś podobnego (op + 3), a więc dodanie 3 do każdego elementu jakiejś listy byłoby (mapcar (op + 3) some-list)raczej (mapcar (lambda (x) (+ 3 x)) some-list). To opmakro sprawi, że otrzymasz funkcję, która pobiera argumenty x y z ...i wywołuje (+ a x y z ...).

W wielu czysto funkcjonalnych językach częściowa aplikacja jest wbudowana w składnię, dzięki czemu nie ma opoperatora. Aby uruchomić częściową aplikację, wystarczy wywołać funkcję z mniejszą liczbą argumentów, niż wymaga. Zamiast generowania "insufficient number of arguments"błędu wynik jest funkcją pozostałych argumentów.

Kaz
źródło
„Currying ... pozwala tworzyć nową funkcję ... poprzez ustalenie niektórych parametrów” - nie, to funkcja typu a -> b -> cnie posiada parametr s (liczba mnoga), to ma tylko jeden parametr c. Po wywołaniu zwraca funkcję typu a -> b.
Max Heiber
4

Dla funkcji

fun add(x, y) = x + y

Ma formę f': 'a * 'b -> 'c

Do oceny wystarczy

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

Do funkcji curry

fun add x y = x + y

Do oceny wystarczy

add 3
val it = fn : int -> int

Gdzie jest to obliczenie częściowe, a konkretnie (3 + y), które następnie można wykonać

it 5
val it = 8 : int

dodaj w drugim przypadku ma formę f: 'a -> 'b -> 'c

To, co robi curry, polega na przekształceniu funkcji, która przyjmuje dwie umowy w jedną, która przyjmuje tylko jedną, zwracającą wynik. Częściowa ocena

Dlaczego miałoby to być potrzebne?

Powiedz xw RHS nie jest zwykłym intem, ale zamiast tego złożonym obliczeniem, którego wypełnienie zajmuje trochę czasu, na wszelki wypadek, dwie sekundy.

x = twoSecondsComputation(z)

Tak teraz wygląda funkcja

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

Rodzaju add : int * int -> int

Teraz chcemy obliczyć tę funkcję dla zakresu liczb, zmapujmy ją

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

W związku z powyższym wynik twoSecondsComputationjest oceniany za każdym razem. Oznacza to, że wykonanie tego obliczenia zajmuje 6 sekund.

Używając kombinacji inscenizacji i curry, można tego uniknąć.

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

Z curry add : int -> int -> int

Teraz można zrobić

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

Trzeba twoSecondsComputationtylko ocenić raz. Aby zwiększyć skalę, zamień dwie sekundy na 15 minut lub dowolną godzinę, a następnie przygotuj mapę dla 100 liczb.

Podsumowanie : Curry jest świetne, gdy używa się go z innymi metodami dla funkcji wyższego poziomu jako narzędzia częściowej oceny. Jego celu nie da się tak naprawdę wykazać samodzielnie.

phwd
źródło
3

Curry umożliwia elastyczną kompozycję funkcji.

Stworzyłem funkcję „curry”. W tym kontekście nie dbam o to, jakiego rodzaju rejestrator dostaję i skąd pochodzi. Nie obchodzi mnie, czym jest akcja ani skąd się bierze. Wszystko, na czym mi zależy, to przetwarzanie moich danych wejściowych.

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.

Zmienna konstruktora jest funkcją, która zwraca funkcję zwracającą funkcję, która pobiera moje dane wejściowe i wykonuje moją pracę. Jest to prosty użyteczny przykład, a nie obiekt w zasięgu wzroku.

śmiertelnik
źródło
2

Curry jest zaletą, gdy nie masz wszystkich argumentów dla funkcji. Jeśli zdarzy ci się w pełni oceniać funkcję, to nie ma znaczącej różnicy.

Curry pozwala uniknąć wspominania o niepotrzebnych jeszcze parametrach. Jest bardziej zwięzły i nie wymaga znalezienia nazwy parametru, która nie koliduje z inną zmienną w zakresie (co jest moją ulubioną korzyścią).

Na przykład, gdy używasz funkcji przyjmujących funkcje jako argument, często znajdziesz się w sytuacjach, w których potrzebujesz funkcji takich jak „dodaj 3 do danych wejściowych” lub „porównaj dane wejściowe ze zmienną v”. Dzięki curry te funkcje można łatwo napisać: add 3i (== v). Bez curry musisz używać wyrażeń lambda: x => add 3 xi x => x == v. Wyrażenia lambda są dwa razy dłuższe i mają niewielką ilość pracy związanej z wybraniem nazwy poza tym, xjeśli jest już xw zasięgu.

Dodatkową korzyścią języków opartych na curry jest to, że pisząc ogólny kod funkcji, nie powstają setki wariantów opartych na liczbie parametrów. Na przykład w języku C # metoda „curry” wymagałaby wariantów Func <R>, Func <A, R>, Func <A1, A2, R>, Func <A1, A2, A3, R> itd. na zawsze. W Haskell odpowiednik Func <A1, A2, R> bardziej przypomina Func <Tuple <A1, A2>, R> lub Func <A1, Func <A2, R >> (i Func <R> jest bardziej jak Func <Jednostka, R>), więc wszystkie warianty odpowiadają pojedynczej sprawie Func <A, R>.

Craig Gidney
źródło
2

Główne rozumowanie, które mogę wymyślić (i nie jestem ekspertem w tej dziedzinie pod żadnym względem) zaczyna wykazywać jego zalety, gdy funkcje zmieniają się z trywialnych na nietrywialne. We wszystkich trywialnych przypadkach z większością koncepcji tego rodzaju nie znajdziesz prawdziwej korzyści. Jednak większość języków funkcjonalnych intensywnie wykorzystuje stos w operacjach przetwarzania. Rozważ PostScript lub Lisp jako przykłady tego. Korzystając z curry, funkcje mogą być układane bardziej efektywnie, a ta korzyść staje się widoczna, gdy operacje stają się coraz mniej banalne. W sposób curry polecenie i argumenty mogą być rzucane na stos w kolejności i wyskakujące w razie potrzeby, aby były uruchamiane we właściwej kolejności.

Peter Mortensen
źródło
1
W jaki sposób wymaganie utworzenia znacznie większej liczby ramek stosu zwiększa wydajność?
Mason Wheeler
1
@MasonWheeler: Nie wiedziałbym, bo powiedziałem, że nie jestem ekspertem od języków funkcjonalnych ani konkretnie curry. Z tego powodu oznaczyłem to wiki społeczności.
Joel Etherton
4
@MasonWheeler Twój punkt ma zdanie na temat sformułowania tej odpowiedzi, ale pozwólcie, że wrócę i powiem, że liczba faktycznie utworzonych ramek stosu zależy od implementacji. Na przykład w pozbawionej spinów maszynie G bez tagów (STG; sposób, w jaki GHC implementuje Haskell) opóźnia faktyczną ocenę, dopóki nie zgromadzi wszystkich (lub przynajmniej tyle, ile wie, że jest wymaganych) argumentów. Nie mogę sobie przypomnieć, czy zrobiono to dla wszystkich funkcji, czy tylko dla konstruktorów, ale myślę, że powinno to być możliwe dla większości funkcji. (Z drugiej strony, koncepcja „ramek stosu” tak naprawdę nie dotyczy STG.)
1

Curry zależy przede wszystkim (ostatecznie nawet) od zdolności do zwrócenia funkcji.

Rozważ ten (wymyślony) pseudo kod.

var f = (m, x, b) => ... zwróć coś ...

Załóżmy, że wywołanie f z mniej niż trzema argumentami zwraca funkcję.

var g = f (0, 1); // zwraca funkcję powiązaną z 0 i 1 (m i x), która akceptuje jeszcze jeden argument (b).

var y = g (42); // wywołaj g z brakującym trzecim argumentem, używając 0 i 1 dla m i x

Możliwość częściowego zastosowania argumentów i odzyskania funkcji wielokrotnego użytku (powiązanej z argumentami, które dostarczyłeś) jest dość przydatna (i OSUSZAJĄCA).

Rick O'Shea
źródło