2012-09-13 13 views
8

Nie sądzę, żebym całkiem rozumiał, jak się pieką, skoro nie widzę żadnej ogromnej korzyści, jaką mogłaby zapewnić. Być może ktoś mógłby mnie oświecić przykładem pokazującym, dlaczego jest tak przydatny. Czy to naprawdę ma zalety i zastosowania, czy jest to po prostu zbyt doceniana koncepcja?Jakie są korzyści z curry?

+2

Poza tym, że twoja żywność jest wyjątkowa, uważam, że jest używana do umożliwienia łańcuchów operacji na zbiorze danych. Powoduje to, że zamiast pisać skomplikowane algorytmy z zagnieżdżonymi pętlami itp., Można to wykonać za pomocą kilku prostych poleceń. – Shmiddty

+2

Czy rozumiesz zalety funkcji lambda? –

+0

@DaxFohl jak w C# lambdas? – user997112

Odpowiedz

12

(Istnieje niewielka różnica między currying i częściowe zastosowanie, chociaż są one ściśle powiązane;., Ponieważ są one często miesza się ze sobą, ja sobie z obu kategoriach)

miejsce gdzie ja sobie sprawę z korzyści pierwszy był kiedy zobaczyłem pokrojone operatory:

incElems = map (+1) 
--non-curried equivalent: incElems = (\elems -> map (\i -> (+) 1 i) elems) 

IMO, jest to zupełnie łatwe do odczytania. Teraz, jeśli typ (+) był (Int,Int) -> Int *, który jest wersją nieuruchwytowaną, to (przeciwnie intuicyjnie) spowodowałby błąd - ale na curry działa zgodnie z oczekiwaniami i ma typ [Int] -> [Int].

Wspomniałeś C# lambdas w komentarzu. W języku C#, można napisać incElems jak więc, biorąc pod uwagę funkcję plus:

var incElems = xs => xs.Select(x => plus(1,x)) 

Jeśli jesteś przyzwyczajony do punktu-free style, zobaczysz, że x tutaj jest zbędne. Logicznie, kod ten można zredukować do

var incElems = xs => xs.Select(curry(plus)(1)) 

który jest okropny z powodu braku automatycznej częściowej aplikacji z C# lambdas. I to jest kluczowy punkt, aby zdecydować, w którym miejscu currying jest rzeczywiście użyteczny: głównie kiedy to się stanie domyślnie.Dla mnie najłatwiejszy do odczytania jest map (+1), a następnie wersja curry powinna być prawdopodobnie unikana, jeśli nie ma naprawdę dobrego powodu.

Teraz, jeśli czytelny, korzyści sumują się do krótsze, bardziej czytelne i mniej zaśmiecone kod - chyba że istnieją pewne nadużywanie punkcie wolne stylu wykonane jest z nim (kocham (.).(.), ale jest. .. special)

Ponadto, rachunek lambda stałby się niemożliwy bez użycia funkcji curried, ponieważ ma tylko jedną wartość (ale także wyższego rzędu).

* Oczywiście w rzeczywistości jest to Num, ale teraz jest bardziej czytelny.


Aktualizacja: jak naprawdę działa curry.

Spójrz na rodzaj plus w C#:

int plus(int a, int b) {..} 

trzeba dać mu krotką wartości - nie w C# kategoriach, ale matematycznie mówionego; nie możesz po prostu pominąć drugiej wartości. Pod względem Haskell, to

plus :: (Int,Int) -> Int, 

które mogą być używane jak

incElem = map (\x -> plus (1, x)) -- equal to .Select (x => plus (1, x)) 

to zbyt wiele znaków typu. Załóżmy, że chcesz częściej robić to częściej w przyszłości. Oto mały pomocnik:

curry f = \x -> (\y -> f (x,y)) 
plus' = curry plus 

co daje

incElem = map (plus' 1) 

Zastosujmy to do wartości betonowej.

incElem [1] 
= (map (plus' 1)) [1] 
= [plus' 1 1] 
= [(curry plus) 1 1] 
= [(\x -> (\y -> plus (x,y))) 1 1] 
= [plus (1,1)] 
= [2] 

Tutaj możesz zobaczyć curry w pracy. Przekształca standardową funkcję funkcji stylu haskell (plus' 1 1) w wywołanie funkcji "tupled" - lub oglądany na wyższym poziomie przekształca "tupled" w wersję "untupled".

Na szczęście przez większość czasu nie musisz się o to martwić, ponieważ jest automatyczna aplikacja częściowa.

+0

Aby wyjaśnić, dlaczego jest (+1) currying? Chcę tylko mieć pewność, że dokładnie rozumiem, co to jest, nie tylko dlatego, że jest dobre. Dzięki – user997112

+0

@ user997112: '(+ 1)' jest aplikacją częściową, używając specjalnej składni dla operatorów infiksów. Jeśli '(+)' zostało nazwane 'dodaj' zamiast tego, curry pozwoliłoby ci napisać' add 1 1' zamiast 'add (1, 1)'. Częściowe zastosowanie tej ostatniej jest o wiele bardziej niezręczne, jak wyjaśnia powyższa odpowiedź. –

+1

@ user997112 - '(+1)' jest aplikacją częściową - '(+)' jest funkcją curry, ponieważ ma typ 'Int -> Int -> Int' zamiast' (Int, Int) -> Int' jak to robi na przykład w języku C#. W Haskell możesz konwertować '(+)' na drugą formę używając 'uncurry' np. uncurry (+) $ (1,1) – Lee

4

Jedną z zalet curry jest to, że pozwala na częściowe zastosowanie funkcji bez potrzeby stosowania jakiejś specjalnej składni/operatora. Prosty przykład:

mapLength = map length 
mapLength ["ab", "cde", "f"] 
>>> [2, 3, 1] 
mapLength ["x", "yz", "www"] 
>>> [1, 2, 3] 

map :: (a -> b) -> [a] -> [b] 
length :: [a] -> Int 
mapLength :: [[a]] -> [Int] 

Funkcja map może być uważany za typu (a -> b) -> ([a] -> [b]) powodu currying, więc gdy length stosuje się jako pierwszy argument, otrzymuje się funkcję mapLength typu [[a]] -> [Int].

+0

Która część twojego kodu to curry? Czy rozumiesz, że definicja mapy (lub długości) zawiera ją w preludium? – user997112

+1

@ user997112 'map' jest funkcją curry. Dlatego możliwe jest napisanie 'map length', aby częściowo ją zastosować (w przeciwieństwie do' \ xs -> map (length, xs) ', które musiałbyś napisać, gdyby było zdefiniowane za pomocą krotek zamiast curry). – sepp2k

+0

@ user997112 Wyjaśniono w odpowiedzi. –

7

To nie jest najlepsza rzecz od czasu krojonego chleba, ale jeśli używasz lambdas i tak, łatwiej jest używać funkcji wyższego rzędu bez użycia składni lambda. Porównaj:

map (max 4) [0,6,9,3] --[4,6,9,4] 
map (\i -> max 4 i) [0,6,9,3] --[4,6,9,4] 

Tego rodzaju konstrukcje pojawią się na tyle często, gdy używasz programowania funkcyjnego, że jest to miły skrót mieć i pozwala myśleć o problemie z nieco wyższym poziomie - you're mapowanie w stosunku do funkcji "max 4", a nie jakiejś przypadkowej funkcji, która została zdefiniowana jako (\i -> max 4 i). To pozwala łatwiej myśleć o wyższym poziomie pośredniczenia:

let numOr4 = map $ max 4 
let numOr4' = (\xs -> map (\i -> max 4 i) xs) 
numOr4 [0,6,9,3] --ends up being [4,6,9,4] either way; 
       --which do you think is easier to understand? 

To powiedziawszy, nie jest to panaceum; czasami parametry twojej funkcji będą niewłaściwą kolejnością tego, co próbujesz zrobić z curry, więc i tak musisz uciekać się do lambda. Kiedy jednak przyzwyczaisz się do tego stylu, zaczniesz się uczyć, jak zaprojektować swoje funkcje, aby dobrze z nim współpracować, a kiedy te neurony zaczną łączyć się z twoim mózgiem, wcześniej skomplikowane konstrukcje mogą zacząć wydawać się oczywiste w porównaniu.

+1

Och, nigdy * nie * musisz uciekać się do lambda (ale często powinieneś). –

+0

Nie rozumiem tego komentarza? – user997112

+3

@ user997112 możesz użyć 'flip' i znajomych, aby utworzyć nowe funkcje z istniejących funkcji, z kolejnością parametrów zmienionych. Więc jeśli masz 'f :: x-> y-> z',' (flip f) y1' będzie równoznaczne z '(\ x -> fx y1)', co pozwala ci na "curry" drugiego parametr. Ale to może (zwykle robi) kończy się wytwarzanie kodu znacznie bardziej niejasne niż tylko za pomocą lambda. –

2

Jest to nieco wątpliwe, aby zapytać, jakie korzyści zmiękczania są bez określenia kontekstu, w którym pytasz pytanie:

  • W niektórych przypadkach, takich jak języków funkcyjnych, Zmiękczanie będzie jedynie być postrzegane jako coś, ma bardziej lokalną zmianę, w której można zamienić rzeczy na jawne domeny z potknięciami. Nie oznacza to jednak, że curry są bezużyteczne w tych językach. W pewnym sensie programowanie za pomocą funkcji curry sprawia, że ​​czujesz się tak, jakbyś programował w bardziej funkcjonalnym stylu, ponieważ bardziej typowo spotykasz się z sytuacjami, w których masz do czynienia z funkcjami wyższego rzędu. Oczywiście, przez większość czasu "wypełnisz" wszystkie argumenty funkcji, ale w przypadkach, w których chcesz użyć funkcji w jej częściowo zastosowanej formie, jest to nieco prostsze w formie curry.Zazwyczaj mówimy naszym początkującym programistom, aby używali tego podczas uczenia się języka funkcjonalnego tylko dlatego, że czuje się lepszy styl i przypomina im, że programują w więcej niż C. Mając takie rzeczy jak curry i uncurry również pomagają w pewnych udogodnieniach w językach programowania funkcjonalnego. , Mogę myśleć o strzałach w Haskell jako szczególnym przykładzie, w którym użyłbyś curry i uncurry trochę, by zastosować rzeczy do różnych części strzały, itd ...
  • W niektórych przypadkach chcesz myśleć o więcej niż programy funkcjonalne, możesz przedstawić currying/uncurrying jako sposób na określenie zasad eliminacji i wprowadzania oraz w konstruktywnej logice, która zapewnia połączenie z bardziej elegancką motywacją, dlaczego istnieje.
  • W niektórych przypadkach, na przykład w Coq, użycie funkcji curried zamiast funkcji kołowych może spowodować różne schematy indukcyjne, z którymi może być łatwiej lub trudniej pracować, w zależności od aplikacji.
3

Currying ma cechy wygody wymienione w innych odpowiedziach, ale często służy uproszczeniu rozumowania o języku lub wdrożeniu kodu znacznie łatwiejszego niż mogłoby być inaczej. Na przykład currying oznacza, że ​​każda funkcja ma typ kompatybilny z a ->b. Jeśli napiszesz jakiś kod, którego typ dotyczy a -> b, ten kod może być w ogóle wykonany z dowolną funkcją, bez względu na to, ile argumentów zajmuje.

Najbardziej znanym przykładem jest klasa Applicative:

class Functor f => Applicative f where 
    pure :: a -> f a 
    (<*>) :: f (a -> b) -> f a -> f b 

i przykład użycia:

-- All possible products of numbers taken from [1..5] and [1..10] 
example = pure (*) <*> [1..5] <*> [1..10] 

W tym kontekście pure i <*> dostosować żadnej funkcji typu a -> b pracować listy typu [a]. Z powodu częściowego zastosowania oznacza to, że można również przystosować funkcje typu a -> b -> c do pracy z [a] i [b] lub a -> b -> c -> d z [a], [b] i [c] i tak dalej.

Powodem jest to działa bo a -> b -> c to samo, co a -> (b -> c):

(+)        :: Num a => a -> a -> a 
pure (+)      :: (Applicative f, Num a) => f (a -> a -> a) 
[1..5], [1..10]     :: Num a => [a] 
pure (+) <*> [1..5]    :: Num a => [a -> a] 
pure (+) <*> [1..5] <*> [1..10] :: Num a => [a] 

Innym, różne zastosowanie zmiękczania Haskell jest to, że pozwala na częściowe zastosowanie typu konstruktorów. Na przykład, jeśli masz tego typu:

data Foo a b = Foo a b 

... to rzeczywiście sens pisać Foo a w wielu kontekstach, na przykład:

instance Functor (Foo a) where 
    fmap f (Foo a b) = Foo a (f b) 

tj Foo jest rodzajem konstruktor dwuparametrowego z rodzaj * -> * -> *; Foo a, częściowe zastosowanie Foo tylko do jednego typu jest konstruktorem typu z rodzajem * -> *. Functor jest klasą typu, która może być tworzona tylko dla konstratur typu * -> *. Ponieważ Foo a jest tego rodzaju, można utworzyć dla niego instancję Functor.

+0

Dobra uwaga. Myślę, że to właśnie próbowałem powiedzieć o rachunku lambda: currying pozwala używać języka, który obsługuje tylko funkcje jednowartościowe, tak jakby miał funkcje wielowartościowe. – phg

+0

@phg, a więc funkcje nieruchliwe (na przykład tupled). –

3

"nie-Zmiękczanie" formą częściowego zastosowania działa tak:

  • Mamy funkcja f : (A ✕ B) → C
  • Chcielibyśmy, aby zastosować go częściowo na niektóre a : A
  • Aby to zrobić, budujemy zamknięcie z a i f (na razie nie oceniamy w ogóle f)
  • Potem jakiś czas później otrzymujemy drugi argument b : B
  • Teraz mamy zarówno argument A i B możemy ocenić f w jego pierwotnej formie ...
  • więc przypomnieć a z zamknięcia i ocenić f(a,b).

Nieco skomplikowane, prawda?

Kiedy f jest curry w pierwszej kolejności, to raczej prostsze:

  • Mamy funkcję f : A → B → C
  • Chcielibyśmy, aby zastosować go częściowo w pewnym a : A - co możemy tylko zrobić : f a
  • Po pewnym czasie otrzymujemy drugi argument: b : B
  • Stosujemy już wycenione f a do b.

tej pory tak ładne, ale ważniejsze niż bycie prostym, to również daje nam dodatkowe możliwości realizacji naszej funkcji: możemy być w stanie zrobić kilka obliczeń tak szybko, jak jest odbierany a argumentem, a te obliczenia wygrał trzeba to zrobić później, nawet jeśli funkcja jest oceniana za pomocą wielu różnych argumentów b!

Aby podać przykład, należy rozważyć this audio filter, nieskończony filtr odpowiedzi impulsu. Działa to tak: dla każdej próbki audio podajemy "funkcję akumulatora" (f) z pewnym parametrem stanu (w tym przypadku prostą liczbą, 0 na początku) i próbką audio. Funkcja wykonuje trochę magii i wypluwa nowy wewnętrzny stani próbkę wyjściową.

Teraz tutaj jest kluczowa bit - jaki rodzaj magii funkcja nie zależy od współczynnika λ, co nie jest dość stała: to zależy zarówno od tego, co częstotliwość odcięcia którą chcemy filtr mieć (ta reguluje "sposób działania filtra") i na podstawie częstotliwości próbkowania, którą przetwarzamy. Niestety, obliczenie λ jest nieco bardziej skomplikowane (lp1stCoeff $ 2*pi * (νᵥ ~*% δs) niż reszta magii, więc nie chcielibyśmy tego robić dla każdej pojedynczej próbki, cały czas dość denerwujący, ponieważ stały się bardzo rzadkie, na pewno nie w każdej próbce audio.

Ale curry oszczędza dzień! Po prostu obliczamy λ, gdy tylko będziemy mieli niezbędne parametry. Następnie, na każdej z wielu próbek audio, które dopiero nadejdzie, wystarczy wykonać pozostałą, bardzo łatwą magię: yⱼ = yⱼ₁ + λ ⋅ (xⱼ - yⱼ₁). Tak więc jesteśmy skuteczni i wciąż utrzymujemy ładny, bezpieczny interfejs czysto funkcjonalny.


Należy zauważyć, że tego rodzaju państwowego marginesie można przeprowadzić bardziej przyjemnie z State lub ST monady, że po prostu nie jest szczególnie korzystne w tym przykładzie

Tak, to jest symbol lambda. Mam nadzieję, że nikogo nie mylę - na szczęście w Haskell jasne jest, że funkcje lambda są pisane z \, a nie z λ.

1

Kiedyś sądziłem, że curry to prosty cukier składniowy, który oszczędza trochę pisania. Na przykład, zamiast pisania

(\ x -> x + 1) 

Mogę jedynie napisać

(+1) 

Ten ostatni jest od razu stają się bardziej czytelne i mniej wpisywanie do rozruchu.

Więc jeśli to tylko wygodny skrót, dlaczego całe zamieszanie?

Cóż, okazuje się, że ponieważ typy funkcji są curry, można napisać kod, który jest polimorficzny w liczbie argumentów funkcji.

Na przykład struktura QuickCheck umożliwia testowanie funkcji poprzez podawanie losowo wygenerowanych danych testowych. Działa na każdej funkcji, której typ wejścia może być generowany automatycznie. Ale z powodu currying, autorzy byli w stanie ustawić go tak, aby działał z każdą liczbą argumentów. Gdyby były curry, nie byłoby innej funkcji testowej dla każdej liczby argumentów - a to byłoby po prostu żmudne.

Powiązane problemy