29/11/2025
W świecie programowania funkcyjnego, zwłaszcza w językach takich jak Haskell, pojęcia takie jak „kurryfikacja” (currying) i „dekurryfikacja” (uncurrying) są fundamentalne. Choć na pierwszy rzut oka mogą wydawać się abstrakcyjne, stanowią one rdzeń elastyczności i potęgi, jaką oferuje Haskell w manipulowaniu funkcjami. Zrozumienie ich różnic i zastosowań jest kluczowe dla każdego, kto chce pisać efektywny i elegancki kod w tym języku. W tym artykule przyjrzymy się głębiej tym dwóm koncepcjom, wyjaśniając, dlaczego funkcje Haskella zachowują się w określony sposób i jak wbudowane funkcje curry i uncurry pomagają nam w transformacji ich formy.

Co to jest Kurryfikacja (Currying)?
Nazwana na cześć logika Haskella Curry’ego, kurryfikacja to technika transformacji funkcji, która przyjmuje wiele argumentów, w sekwencję funkcji przyjmujących pojedyncze argumenty. W Haskellu, jest to domyślny sposób działania funkcji. Kiedy definiujemy funkcję, która wydaje się przyjmować wiele argumentów, na przykład add :: Int -> Int -> Int, w rzeczywistości definiujemy funkcję, która przyjmuje jeden argument typu Int i zwraca inną funkcję, która z kolei przyjmuje kolejny argument typu Int i dopiero wtedy zwraca ostateczny wynik typu Int. Możemy to zapisać jako add :: Int -> (Int -> Int). Strzałki w typach funkcji są domyślnie prawostronnie asocjacyjne, co oznacza, że a -> b -> c jest interpretowane jako a -> (b -> c).
Ta wbudowana charakterystyka Haskella ma ogromne konsekwencje, z których najważniejszą jest możliwość częściowego aplikowania funkcji. Oznacza to, że możemy wywołać funkcję z mniejszą liczbą argumentów niż jest to wymagane, a w zamian otrzymamy nową funkcję, która "pamięta" już podane argumenty i czeka na pozostałe.
add :: Int -> Int -> Int add x y = x + y
Gdy wywołamy add 3, nie otrzymujemy błędu, ale nową funkcję, którą możemy nazwać add3. Ta funkcja add3 jest typu Int -> Int i jest gotowa przyjąć drugi argument, aby dokończyć sumowanie:
ghci> let add3 = add 3 ghci> add3 4 7 ghci> add 3 4 7
Wewnętrznie, nawet add 3 4 jest traktowane jako (add 3) 4. Ta elastyczność jest niezwykle potężna w programowaniu funkcyjnym, umożliwiając tworzenie bardziej modularnego i reużywalnego kodu.
Kiedy Potrzebujemy Zmienić Formę Funkcji?
Chociaż domyślna kurryfikacja jest potężna, czasami chcemy, aby funkcja przyjmowała argumenty w postaci krotki (ang. tuple), czyli połączonego zestawu wartości. Jest to szczególnie przydatne, gdy pracujemy z funkcjami wyższego rzędu, takimi jak map, które oczekują funkcji przyjmującej pojedynczy argument.
Rozważmy funkcję map, która ma typ map :: (a -> b) -> [a] -> [b]. Przyjmuje ona funkcję f (typu a -> b) i listę [a], a następnie stosuje f do każdego elementu listy, zwracając [b].
Załóżmy, że mamy listę par liczb, np. [(1,3), (4,9), (3,7)], i chcemy stworzyć nową listę, w której każdy element jest iloczynem liczb z odpowiadającej mu pary. Idealnie, chcielibyśmy użyć funkcji multiply (typu Int -> Int -> Int) w połączeniu z map.

multiply x y = x * y
Gdybyśmy spróbowali map multiply [(1,3), (4,9), (3,7)], napotkalibyśmy problem. map oczekuje funkcji, która przyjmuje pojedynczy element listy (w tym przypadku parę (Int, Int)) i zwraca pojedynczą wartość. Jednakże multiply jest funkcją kurryfikowaną, która oczekuje dwóch oddzielnych argumentów Int, a nie jednej krotki (Int, Int). Typy nie pasują. multiply ma typ Int -> Int -> Int, a my potrzebujemy funkcji typu (Int, Int) -> Int.
Możemy to obejść za pomocą wyrażenia lambda:
ghci> map (\(x, y) -> x * y) [(1,3), (4,9), (3,7)] [3,36,21]
Jednak Haskell oferuje bardziej eleganckie rozwiązanie w postaci wbudowanych funkcji curry i uncurry, które służą do konwersji między tymi dwoma formami funkcji.
Funkcje curry i uncurry w Haskellu
Biblioteka standardowa Haskella dostarcza dwie kluczowe funkcje do transformacji między funkcjami kurryfikowanymi a tymi, które przyjmują krotki.
Funkcja uncurry
Funkcja uncurry służy do przekształcania funkcji kurryfikowanej (typu a -> b -> c) w funkcję, która przyjmuje pojedynczy argument w postaci krotki (a, b) i zwraca c. Jest to niezwykle przydatne, gdy musimy dopasować funkcję wieloargumentową do kontekstu, który oczekuje funkcji jednoargumentowej przyjmującej krotkę.
Typ uncurry:
uncurry :: (a -> b -> c) -> ((a, b) -> c)
Definicja (jak w bibliotece standardowej):
uncurry f (a, b) = f a b
Wracając do naszego przykładu z map i multiply, uncurry jest dokładnie tym, czego potrzebujemy:
ghci> multiply x y = x * y ghci> map (uncurry multiply) [(1,3), (4,9), (3,7)] [3,36,21]
Dzięki uncurry multiply, przekształciliśmy multiply (które oczekuje Int i Int) w funkcję, która oczekuje pojedynczej krotki (Int, Int). Ta nowa funkcja doskonale pasuje do oczekiwań map.
Funkcja curry
Funkcja curry jest odwrotnością uncurry. Służy do przekształcania funkcji, która przyjmuje pojedynczy argument w postaci krotki (a, b) i zwraca c, w funkcję kurryfikowaną typu a -> b -> c. Jest to użyteczne, gdy masz funkcję "niekurryfikowaną" (tzn. przyjmującą krotkę) i chcesz wykorzystać zalety częściowego aplikowania funkcji lub po prostu dopasować ją do kontekstu oczekującego funkcji kurryfikowanej.

Typ curry:
curry :: ((a, b) -> c) -> (a -> b -> c)
Definicja (jak w bibliotece standardowej):
curry f a b = f (a, b)
Przykład użycia:
Załóżmy, że masz funkcję pairSum, która sumuje elementy krotki:
pairSum :: (Int, Int) -> Int pairSum (x, y) = x + y
Jeśli chcesz użyć pairSum w sposób kurryfikowany, tak aby można było ją częściowo aplikować, możesz użyć curry:
ghci> let curriedPairSum = curry pairSum ghci> curriedPairSum 1 2 3 ghci> let addOne = curriedPairSum 1 ghci> addOne 5 6
curriedPairSum teraz zachowuje się jak zwykła funkcja Haskella z dwoma argumentami, umożliwiając częściowe aplikowanie.
Tabela Porównawcza: curry vs uncurry
| Cecha | curry | uncurry |
|---|---|---|
| Cel | Przekształca funkcję przyjmującą krotkę w funkcję kurryfikowaną. | Przekształca funkcję kurryfikowaną w funkcję przyjmującą krotkę. |
| Typ wejściowy funkcji | (a, b) -> c | a -> b -> c |
| Typ wyjściowy funkcji | a -> b -> c | (a, b) -> c |
| Kiedy używać? | Gdy chcesz częściowo aplikować funkcję, która początkowo przyjmuje krotkę. | Gdy funkcja wyższego rzędu (np. map) oczekuje funkcji przyjmującej pojedynczy argument (krotkę), ale masz funkcję kurryfikowaną. |
| Relacja | Odwrotność uncurry. | Odwrotność curry. |
Dlaczego Funkcje Haskell są Domyślnie Kurryfikowane?
Istnieje kilka kluczowych powodów, dla których Haskell domyślnie wykorzystuje kurryfikację, czyniąc ją integralną częścią swojego modelu funkcji:
- Elegancja i Prostota Teoretyczna: W systemach formalnych i teoriach typów, traktowanie wszystkich funkcji jako przyjmujących tylko jeden argument (i zwracających albo wynik, albo inną funkcję) znacznie upraszcza rozumowanie i dowody. Umożliwia to bardziej spójną i minimalistyczną teorię funkcji.
- Ułatwienie Częściowego Aplikowania: Jak już wspomniano, kurryfikacja jest fundamentem dla częściowego aplikowania funkcji. Ta technika pozwala na tworzenie nowych, bardziej specjalistycznych funkcji z istniejących, poprzez "zablokowanie" niektórych argumentów. Jest to niezwykle potężne dla kompozycji funkcji i pisania zwięzłego, deklaratywnego kodu.
- Wsparcie dla Funkcji Wyższego Rzędu: Wiele funkcji w Haskellu to funkcje wyższego rzędu, które przyjmują inne funkcje jako argumenty lub zwracają je jako wyniki. Kurryfikacja sprawia, że interakcja z nimi jest bardziej naturalna i elastyczna. Na przykład,
mapdziała bezproblemowo z częściowo zaaplikowanymi funkcjami. - Zwięzłość Kodu: Dzięki częściowemu aplikowaniu i kompozycji, programiści mogą pisać mniej boilerplate'u i wyrażać złożone operacje w bardzo zwięzły sposób. Zamiast definiować nowe funkcje lambda dla każdego przypadku, często można po prostu częściowo zaaplikować istniejące funkcje.
Często Zadawane Pytania (FAQ)
Czym jest częściowo zaaplikowana funkcja?
Częściowo zaaplikowana funkcja to wynik wywołania funkcji kurryfikowanej z mniejszą liczbą argumentów niż jest to wymagane. Zamiast końcowego wyniku, otrzymujemy nową funkcję, która "pamięta" już podane argumenty i czeka na pozostałe, aby dokończyć obliczenia.
Czy mogę kurryfikować/dekurryfikować funkcje z więcej niż dwoma argumentami?
Standardowe funkcje curry i uncurry w Haskellu są zdefiniowane dla funkcji przyjmujących dwa argumenty (lub krotki dwuelementowe). Jeśli potrzebujesz obsługi większej liczby argumentów, musiałbyś zdefiniować własne wersje curryN i uncurryN lub użyć krotki o większej liczbie elementów, np. (a, b, c) -> d i a -> b -> c -> d. W praktyce często używa się kompozycji lub wyrażeń lambda dla bardziej złożonych przypadków.
Czy kurryfikacja jest unikalna dla Haskella?
Nie, kurryfikacja jest koncepcją z logiki kombinatorycznej i teorii funkcji, która jest stosowana w wielu językach programowania funkcyjnego (np. Scala, OCaml, F#) oraz w językach wieloparadygmatycznych (np. JavaScript, Python) poprzez biblioteki lub manualne implementacje. Haskell po prostu czyni ją domyślnym zachowaniem dla wszystkich funkcji, co jest jedną z jego wyróżniających cech.
Czy zawsze powinienem używać curry i uncurry?
Nie zawsze. Często, zwłaszcza dla prostych przypadków, użycie wyrażenia lambda (np. \(x, y) -> x * y) jest równie czytelne i efektywne. Funkcje curry i uncurry są najbardziej przydatne, gdy chcemy przekształcić istniejącą funkcję na inną formę, aby lepiej pasowała do kontekstu funkcji wyższego rzędu lub gdy chcemy wykorzystać ich symboliczną naturę. Wybór zależy od czytelności i złożoności danego przypadku.
Podsumowanie
Kurryfikacja i dekurryfikacja są fundamentalnymi koncepcjami w Haskellu, które umożliwiają niezwykłą elastyczność w manipulowaniu funkcjami. Domyślne kurryfikowanie funkcji w Haskellu, wraz z możliwością częściowego aplikowania, stanowi o sile i elegancji tego języka. Funkcje curry i uncurry są narzędziami, które pozwalają nam świadomie przełączać się między różnymi reprezentacjami funkcji – od tych, które przyjmują sekwencje argumentów, do tych, które przyjmują pojedyncze krotki. Zrozumienie tych mechanizmów nie tylko pogłębia wiedzę o Haskellu, ale także otwiera drogę do pisania bardziej idiomatycznego, zwięzłego i potężnego kodu funkcyjnego. Opanowanie ich to krok milowy w kierunku biegłości w programowaniu w Haskellu.
Zainteresował Cię artykuł Curry i Uncurry w Haskellu: Pełne Zrozumienie? Zajrzyj też do kategorii Kulinaria, znajdziesz tam więcej podobnych treści!
