Sytuacja kobiet w IT w 2024 roku
17.02.20205 min
Morgan Kenyon

Morgan KenyonSoftware EngineerPaycom

Programowanie funkcyjne: czym jest currying

Dowiedz się więcej o curryingu czyli rozwijaniu funkcji, które jest powszechnie wykorzystywane w językach funkcyjnych.

Programowanie funkcyjne: czym jest currying

Jakiś czas temu obejrzałem talk Scotta Wlaschina na konferencji o programowaniu funkcyjnym. Powiedział, że te często zniechęca do siebie ludzi swoją terminologią. Terminy jak rozwijanie funkcji, częściowe stosowanie, funktor, czy monada, mogą przyprawić o dreszcze. 

Łatwo jest odnieść wrażenie, że są to super skomplikowane rzeczy, gdy nic się o nich nie wie.

Programowanie obiektowe ma podobną terminologię, np. hermetyzacja, polimorfizm, dziedziczenie. Mieliśmy sporo do czynienia z tymi terminami, więc przestały w końcu straszyć. 

W tym artykule zajmiemy się tematem rozwijania funkcji.

Parametry Metod w programowaniu obiektowym

W większość obiektowych języków programowania (C#, Java, Python i tak dalej) metoda ma pewną liczbę parametrów.

Weźmy na przykład metodę C# do łączenia ciągów znaków.

public string ConcateStrings(string a, string b)
{
    return a + b;
}


Metoda ta przyjmuje dwa ciągi znaków jako wejście i zwraca jeden ciąg na wyjściu.

Za każdym razem jak wywołujesz tę metodę, musisz podać dwa parametry. Jeśli tego nie zrobisz, kompilator C# się odezwie. 

Parametry funkcji w programowaniu funkcyjnym 

W większości funkcyjnych języków programowania (F#, Elm, Haskell i tak dalej) parametry funkcji są definiowane i używane inaczej.  

Oto jak definiujemy funkcję w języku Elm, która potrzebuje dwóch ciągów na wejściu, zanim zwróci ciąg na wyjściu.

concateString : String -> String -> String
concateString a b =
  a ++ b


Zauważcie, że w Elm definicja funkcji jest w oddzielnej linijce od implementacji. Dzieje się tak w wielu językach funkcyjnych. 

Zauważcie też, jak definicja jest zapisana w innym formacie: “string -> string -> string”. Jeśli nie miało się wcześniej do czynienia z funkcyjnym językiem programowania, może się to wydać lekko dziwne. 

Definiowanie podobnej funkcji w F# wygląda trochę inaczej.

let concateString (a: string) (b: string) =
    a + b


Sygnatura jest jednak taka sama. Oto screenshot tej funkcji w VS Code używającym wtyczki lonide.


lonide dodał definicję funkcji (// string -> string -> string) automatycznie. To samo dzieje się w Elm. 

Pomimo, że przyjmuje ona dwa ciągi jako wejście, mogę podać tylko jeden to kompilator nie będzie miał z tym problemu.

//even though concateString takes two inputs
//F# doesn't complain when I only give it one
let concateWordWith = concateString "Word"


Sygnatura concateWordWith to teraz “string -> string”.

Albo mogę zastosować dwa parametry, ale w dwóch nawiasach.

let houndDog = concateString ("Hound") ("Dog")


Dlaczego? Dzięki czemuś, co określa się jako currying.

Czym jest Currying

Currying, znane również jako rozwijanie funkcji, zasadniczo się różni od innych sposobów obsługi parametrów funkcji. W językach obiektowych to wszystko albo nic - jeśli mam 3 inputy, to muszę wszystkich 3 użyć, bo inaczej kompilator wyrzuca błąd. 

Currying zapewnia większą elastyczność. Pozwalają zbudować parametry wejściowe w kilku krokach. Input można zatem podawać stopniowo, najpierw jeden, potem drugi i na końcu ostatni zaraz przed skonsumowaniem go.

Mówiąc inaczej, jeśli dana funkcja ma 3 inputy i oddajesz jej tylko jeden, zwróci ona funkcję z 2 inputami. Można potem dać tej funkcji jeden input i otrzymamy funkcję z 1 inputem. Gdy damy tej funkcji 1 input to otrzymamy w końcu output.

//string -> string -> string -> string -> string
let concateFourStrings (a: string) (b: string) (c: string) (d: string) =
    a + b + c + d
    
//string -> string -> string -> string
let concateThreeStrings = 
    concateFourStrings "first"

//string -> string -> string
let concateTwoStrings =
    concateThreeStrings " second"
    
//string -> string
let concateOneString = 
    concateTwoStrings " third"
    
//string
let finalString =
    concateOneString " fourth"


Jeżeli zobaczysz funkcję o sygnaturze "string -> string -> string", to w świecie obiektowym opisalibyśmy to tak:

Funkcja przyjmuje dwa ciągi znaków jako wejście i zwraca jeden ciąg znaków na wyjściu.

Bardziej funkcyjnym opisem byłoby:

Funkcja przyjmuje ciąg znaków, następnie zwraca funkcję pośrednią, która przyjmuje ciąg znaków i zwraca kolejny ciąg znaków.

Ze względu na currying, w większości języków funkcyjnych nie istnieje coś takiego jak funkcja z wieloma parametrami. W sekwencji pośrednich funkcji, wiele parametrów jest tak naprawdę aplikowanych pojedynczo. Kompilator robi to automatycznie.

Właściwie to Wikipedia definiuje currying jako operację polegającą na przekształcaniu funkcji, która pobiera parę argumentów do przetworzenia w sekwencji funkcji, każdej z pojedynczym argumentem. Wychodzi więc na to, że tworzy to oddzielną funkcję z każdego argumentu. 

Dlaczego warto używać rozwijania funkcji

Teraz, gdy już wiemy, czym jest currying, warto się zastanowić, dlaczego warto go używać. Czy jest to jedna z tych skomplikowanych rzeczy, która utrudni nam życie?

W językach, które wspierają currying, zapewnia on dużą elastyczność, jeżeli chodzi o sposób wywoływania funkcji. 

Oto przykład w C#, w którym dodaliśmy 2 do każdej liczby na liście.

public int Add(int a, int b)
{
    return a + b;
}
var numberList = new List<int>() { 1, 2, 3, 4, 5 };
var numberListPlus2 = numberList.Select(n => Add(2, n));
//numberListPlus2 = [3, 4, 5, 6, 7]


Całkiem prosty kod. Oto, jak można to samo zrobić w F# bez rozwijania funkcji*. 

let add (a: int32) (b: int32) =
    a + b
let numberList = [1;2;3;4;5]
let numberListPlus2 = numberList |> List.map (fun x -> add 2 x)
//numberListPlus2 = [3;4;5;6;7]


Raczej dość jasne. Ponieważ jednak F# rozwija funkcje, oto jak to zrobić w ich stylu:

let add (a: int32) (b: int32) =
    a + b
let numberList = [1;2;3;4;5]
let numberListPlus2 = numberList |> List.map (add 2)
//numberListPlus2 = [3;4;5;6;7]


Funkcja add bierze dwa inputy, ale w powyższym przykład dajemy jej jeden input (2). Następnie List.map daje drugi input, z których każdy jest przyporządkowany jednemu elementowi na liście.

To nie jest jednak wielkie odkrycie. W pracy piszę w C#, więc cały czas zajmuję się zwiniętymi funkcjami. Jeśli jednak spędzi się trochę czasu w języku z curryingiem, to zaczyna się tęsknić za drobnymi rzeczami, które bardzo ułatwiają pracę. 

*Każda funkcja jest automatycznie rozwijana, więc nie jest to do końca prawdziwe stwierdzenie. Różni się jednak od następnego przykładu.

Podsumowanie

Warto znać currying, jeżeli pracuje się z językami funkcyjnymi. To świetny ficzer, który często się przydaje. 

Jest to również jeden z wielu konceptów, którego nie ma w świecie programowania obiektowego. Mam nadzieję, że ten tekst zachęci Was do lepszego zapoznania się z programowaniem funkcyjnym. 

<p>Loading...</p>