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?
źródło
Odpowiedzi:
Dzięki funkcjom curry łatwiejsze jest korzystanie z bardziej abstrakcyjnych funkcji, ponieważ można się specjalizować. Powiedzmy, że masz funkcję dodawania
i że chcesz dodać 2 do każdego członka listy. W Haskell zrobiłbyś to:
Tutaj składnia jest lżejsza niż w przypadku konieczności utworzenia funkcji
add2
lub jeśli musiałbyś wykonać anonimową funkcję lambda:
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:
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ą:
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
map
lubfilter
. 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.źródło
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ć:
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
. Typmap
można zapisać na dwa sposoby: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
map
na 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)
wf
, więcf
nadal 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:Nie można tego łatwo zrobić w przypadku języków, w których od razu pojawia się pomysł na wiele argumentów!
źródło
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.
źródło
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.
źródło
(Podam przykłady w Haskell.)
Podczas używania języków funkcjonalnych bardzo wygodnie jest częściowo zastosować funkcję. Podobnie jak u Haskella
(== x)
jest funkcja, która zwraca,True
jeśli jej argument jest równy podanemu terminowix
:bez curry, mielibyśmy nieco mniej czytelny kod:
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:
Tutaj widzimy
==
jako funkcję oda
doa -> Bool
iany
jako funkcję oda -> Bool
do[a] -> Bool
. Po prostu je komponując, otrzymujemy wynik. Wszystko to dzięki curry.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 as
partition
(< 10)
<
([Int],[Int])
++
++
Rzeczywiście,
(uncurry (++) . partition (< 10)) [4,12,11,1]
ocenia na[4,1,12,11]
.Istnieją również ważne zalety teoretyczne:
(a, b) -> c
naa -> (b -> c)
oznacza, że wynik tej ostatniej funkcji jest typub -> c
. Innymi słowy, wynik jest funkcją.źródło
mem x lst = any (\y -> y == x) lst
? (Z odwrotnym ukośnikiem).Curry to nie tylko cukier składniowy!
Rozważ podpisy typu
add1
(niespieszne) iadd2
(curry):(W obu przypadkach nawiasy w podpisie są opcjonalne, ale dla jasności zamieściłem je).
add1
Jest to funkcja, która bierze 2-krotkiint
iint
i zwracaint
.add2
jest funkcją, która przyjmujeint
i zwraca inną funkcję, która z kolei przyjmujeint
i zwraca anint
.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:
Teraz widzimy różnicę między
add1
iadd2
wyraźniej.add1
zostaje sprawdzony z 2-krotkami:ale
add2
zostaje wywołany za pomocą an,int
a następnie jego wartość zwracana jest wywołana przez innąint
:EDYCJA: Podstawową zaletą curry jest to, że otrzymujesz częściową aplikację za darmo. Powiedzmy, że chcesz funkcji typu
int -> int
(powiedzmy,map
nad listą), która dodała 5 do jej parametru. Mógłbyś pisaćaddFiveToParam x = x+5
lub 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
!źródło
Curry to po prostu cukier składniowy, ale myślę, że trochę nie rozumiesz, co robi cukier. Biorąc swój przykład,
jest właściwie cukrem syntaktycznym
Oznacza to, że (dodaj x) zwraca funkcję, która pobiera argument y i dodaje x do 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:
Wynik byłby
[3,4,5]
.Z drugiej strony, jeśli chcesz podsumować każdą krotkę na liście, funkcja addTuple idealnie pasuje.
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:
źródło
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
W każdym przypadku zmienna typu
c
moż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.źródło
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ę:
Przy wiązaniu 1 do x i częściowym zastosowaniu tego do powyższej funkcji
f(x,y,z)
otrzymasz: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:Gdzie
f''() = 1 + 2 + 3
:;Teraz w dowolnym momencie, można wybrać, aby ocenić
f
,f'
czyf''
. Więc mogę zrobić:lub
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ę:
Jeśli to zrobisz, uzyskasz łańcuch 3 funkcji:
Gdzie:
Teraz, jeśli zadzwonisz
f'(x)
zx = 1
:Otrzymałeś nową funkcję:
Jeśli zadzwonisz
g(y)
zy = 2
:Otrzymałeś nową funkcję:
Wreszcie, jeśli zadzwonisz
h(z)
zz = 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ę:
Zamiast tego możesz napisać zamknięcie:
Gdzie:
f'
jest zamkniętyx
. Oznacza to, żef'
można odczytać wartość x, która jest w środkuf
.Więc jeśli miałbyś zadzwonić
f
zx = 1
:Dostaniesz zamknięcie:
Teraz, jeśli zadzwoniłeś za
closureOfF
pomocąy = 2
iz = 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.
źródło
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)
. Toop
makro sprawi, że otrzymasz funkcję, która pobiera argumentyx 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
op
operatora. 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.źródło
a -> b -> c
nie posiada parametr s (liczba mnoga), to ma tylko jeden parametrc
. Po wywołaniu zwraca funkcję typua -> b
.Dla funkcji
Ma formę
f': 'a * 'b -> 'c
Do oceny wystarczy
Do funkcji curry
Do oceny wystarczy
Gdzie jest to obliczenie częściowe, a konkretnie (3 + y), które następnie można wykonać
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
Powiedz
x
w 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.Tak teraz wygląda funkcja
Rodzaju
add : int * int -> int
Teraz chcemy obliczyć tę funkcję dla zakresu liczb, zmapujmy ją
W związku z powyższym wynik
twoSecondsComputation
jest 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ąć.
Z curry
add : int -> int -> int
Teraz można zrobić
Trzeba
twoSecondsComputation
tylko 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.
źródło
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.
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.
źródło
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 3
i(== v)
. Bez curry musisz używać wyrażeń lambda:x => add 3 x
ix => x == v
. Wyrażenia lambda są dwa razy dłuższe i mają niewielką ilość pracy związanej z wybraniem nazwy poza tym,x
jeśli jest jużx
w 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>.
źródło
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.
źródło
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).
źródło