CurryingCzęść NR.7 Gdzie leży serce programowania funkcjonalnego. Oczywiście w jego funkcjach, które są składową większego algorytmu. Haskell Curry był matematykiem i to od niego wywodzi się termin Currying, jak i cały język programowania Haskell.
Currying sprawia, że jesteśmy w stanie zobaczyć wszystkie funkcje jako funkcje jednoparametrowe bez względu na to, ile parametrów tak naprawdę potrzebujemy do wyliczeń i działania.
Jak to jest możliwe? Przecież gdzieś te parametry muszą być? Jest to jednak prostsze niż się wydaje.
Otwiera nas to na dzielenie aplikacji na mniejsze elementy. Jest to jedna z głównych esencji każdego języka funkcjonalnego. Jak to wygląda w C#, który do końca nie jest językiem funkcjonalnym.
Zmniejszenie ilości parametrów
W większości języków .NET lista parametrów jest zadeklarowana statycznie. Oznacza to, że metody lub funkcje muszą być wywołane ze wszystkimi parametrami razem.
Jako programista C# zapewne się zastanawiasz, jaki byłby sens wysłania niekompletnej listy parametrów do metody. Tylko że właśnie na tym polega Currying.
Currying polega na rozdzieleniu listy parametrów do tego stopnia, że istnieje możliwość wywołania funkcji mając niekompletną listę parametrów.
A co z resztą tych parametrów? Są wypełnione wcześniej przez pewne stałe. Jak to jednak działa? Zaraz się przekonasz.
W językach obiektowych ta filozofia może wydawać się dziwna, a nawet sprzeczna z tym, co robimy na co dzień. Jednakże funkcjonalna alternatywa ma swoje zalety, zwłaszcza jeśli chodzi o podzielenie funkcji i ich ponowne użycie.
Co to jest ten Curriying
Currying jest to transformacja. A każda transformacja Currying-owa zaczyna się od funkcji, która ma wiele parametrów. Naszym zadaniem jest skonwertować funkcję z wieloma parametrami na sekwencje wywołań funkcji jednoparametrowych.
Każda więc funkcja ma przyjmować i zwracać kolejną funkcję. Oto sposób jak mieć zawsze funkcję, która przyjmie zawsze jeden parametr do wywołania, który będzie wysłany dalej.
Na końcu tego łańcucha w wywołaniach wszystkie parametry będą dostępne sprawiając, że ten łańcuch będzie działał dokładnie tak samo, jak oryginalny algorytm.
Spójrzmy na kod, aby wszystko było jasne. Łatwo będzie to zrozumieć patrząc na anonimowe funkcje według składni C# 2.0.
Func<double, double, double> sub =
delegate (double x, double y) {
return x - y;
};};
Oto funkcja, która przyjmuje dwa parametry i zwraca wynik ich odejmowania. Wywołując tę funkcję muszę przesłać dwa parametry x i y.
Korzystając z techniki Currying tworzę teraz funkcję, która akceptuje tylko jeden parametr X. Ta utworzona funkcja zwraca kolejną funkcję, która potrzebuje parametru Y. Ostatecznie ta druga funkcja zwraca wynik odejmowania.
Func<double, Func<double, double>> curriedSub =
delegate (double x) {
return delegate (double y)
{
return x - y;
};
};
Składnia może wydawać się dziwna. C# nie posiada interfejsów dla zmiennych, które przetrzymują anonimowe funkcje. Co prawda mamy delegaty oraz wyrażenia lambda, ale w językach funkcjonalnych jest to dużo prostsze.
W C# musimy podać dokładny typ zwracany przy deklaracji nowej funkcji. W językach funkcjonalnych, gdy funkcja zwraca kolejne funkcje, kod wywołujący jest wolny od ścisłego typu deklaracji. Magiczne słowo kluczowe VAR jest wtedy używane.
Te sam mechanizm transformacji możemy zastosować do wyrażeń lambda. Zapis będzie jednak mniej czytelny.
Mamy więc funkcję odejmującą.
Func<double, double, double> sub2 = (x, y) => x - y;
Zamieniajmy ją w następujący sposób przy użyciu Currying.
Func<double, Func<double, double>> curriedSub2 = x => y => x + y;
Jak widzisz zapis jest dużo krótszy, ale kod będzie działał tak samo.
Oczywiście taka technika może być zastosowana do każdej funkcji bez względu na ilość parametrów. Powiedzmy, że mamy funkcję, która przyjmuje 4 parametry i zwraca liczbę ułamkową.
Func<int, bool, byte, float, double> fun;
Po Curringu będzie ona wyglądać tak.
Func<int, Func<bool, Func<byte, Func<float, double>>>> fun2;
Każda funkcja będzie przyjmowała tylko jeden parametr i zwracała kolejną funkcję potrzebującą następny parametr.
Automatyczny Currying z FCSlib
Jak ten proces można sobie ułatwić? Można go zautomatyzować korzystając z funkcji transformującej.
public static Func<T1, Func<T2, TR>> Curry<T1, T2, TR>
(this Func<T1, T2, TR> func)
{
return p1 => p2 => func(p1, p2);
}
Oto metoda, która zamieni funkcję po Currying-u. Taka metoda zakłada, przyjęcie funkcji dwuparametrowej. Oznacza to, że aby mieć prawdziwą automatyzację musielibyśmy napisać wiele takich transformat dla różnych funkcji z różną ilością parametrów.
Na pomoc jednak przychodzi biblioteka FCSlib, która już zawiera metody rozszerzeniowe dla wszystkich delegat, aby jest przetransformować w ciąg wywołań funkcji.
Oto lista metod, których już nie musimy pisać. Mamy tutaj argumenty przyjmujące delegaty generyczne Func i Action z różnymi ilościami parametrów.
public static Func<T1, Action<T2>> Curry<T1, T2>(this Action<T1, T2> action);
public static Func<T1, Func<T2, TR>> Curry<T1, T2, TR>(this Func<T1, T2, TR> func);
public static Func<T1, Func<T2, Action<T3>>> Curry<T1, T2, T3>(this Action<T1, T2, T3> action);
public static Func<T1, Func<T2, Func<T3, TR>>> Curry<T1, T2, T3, TR>(this Func<T1, T2, T3, TR> func);
public static Func<T1, Func<T2, Func<T3, Action<T4>>>> Curry<T1, T2, T3, T4>(this Action<T1, T2, T3, T4> action);
public static Func<T1, Func<T2, Func<T3, Func<T4, Action<T5>>>>> Curry<T1, T2, T3, T4, T5>(this Action<T1, T2, T3, T4, T5> action);
public static Func<T1, Func<T2, Func<T3, Func<T4, TR>>>> Curry<T1, T2, T3, T4, TR>(this Func<T1, T2, T3, T4, TR> func);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Action<T6>>>>>> Curry<T1, T2, T3, T4, T5, T6>(this Action<T1, T2, T3, T4, T5, T6> action);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, TR>>>>> Curry<T1, T2, T3, T4, T5, TR>(this Func<T1, T2, T3, T4, T5, TR> func);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, TR>>>>>> Curry<T1, T2, T3, T4, T5, T6, TR>(this Func<T1, T2, T3, T4, T5, T6, TR> func);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, Action<T7>>>>>>> Curry<T1, T2, T3, T4, T5, T6, T7>(this Action<T1, T2, T3, T4, T5, T6, T7> action);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, Func<T7, Action<T8>>>>>>>> Curry<T1, T2, T3, T4, T5, T6, T7, T8>(this Action<T1, T2, T3, T4, T5, T6, T7, T8> action);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, Func<T7, TR>>>>>>> Curry<T1, T2, T3, T4, T5, T6, T7, TR>(this Func<T1, T2, T3, T4, T5, T6, T7, TR> func);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, Func<T7, Func<T8, Action<T9>>>>>>>>> Curry<T1, T2, T3, T4, T5, T6, T7, T8, T9>(this Action<T1, T2, T3, T4, T5, T6, T7, T8, T9> action);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, Func<T7, Func<T8, TR>>>>>>>> Curry<T1, T2, T3, T4, T5, T6, T7, T8, TR>(this Func<T1, T2, T3, T4, T5, T6, T7, T8, TR> func);
public static Func<T1, Func<T2, Func<T3, Func<T4, Func<T5, Func<T6, Func<T7, Func<T8, Func<T9, TR>>>>>>>>> Curry<T1, T2, T3, T4, T5, T6, T7, T8, T9, TR>(this Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, TR> func);
Teraz możemy zmieniać istniejące już funkcje.
Func<double, double, double> mutli = (x, y) => x * y;
var curriedMulti = FCSlib.Functional.Curry(mutli);
Słowo kluczowe var jest tutaj bardzo pomocne, gdyż nie musimy podawać skomplikowanej definicji typu generycznej delegaty Func po Currying-u.
Var więc zapisuje niejawnie typ generycznej delegaty. Co ciekawe nic nie stoi na przeszkodzie, aby podać do metody generycznej Curry od razu wyrażenie lambda. Kompilator będzie typował to wyrażenie lambda za nas. Wydaje się to bardzo ciekawe, bo istnieje tona przypadków, gdy kompilator nie będzie mógł tego zrobić.
var curriedMutli2 = FCSlib.Functional.Curry<double, double, double>
((x, y) => x * y);
Niemożliwe jest jednak umieszczenie wyrażenia lambda do słowa kluczowego Var. Zapewne kompilator wtedy nie wie jak utworzyć typ delegaty generycznej Func z wyrażenia lambda.
Dla zmiennej możemy wywołać metodę rozszerzeniową.
Func<double, double, double> div = (x, y) => x / y;
var curriedMult = div.Curry();
Dla samego wyrażenia lambda nie można wywołać metody rozszerzeniowej, bo kompilator nie wie, co to jest.
((double x, double y) = > x / y).Curry();
Wywołanie funkcji po Curring-u
Jak wywołujemy funkcję po Currying-u? Skoro otrzymujemy funkcję to naturalnie wywołujemy te łańcuchy, aż do otrzymania właściwego wyniku.
Func<double, double, double> mutli = (x, y) => x * y;
var curriedMulti = FCSlib.Functional.Curry(mutli);
var curriedMutli2 = FCSlib.Functional.Curry<double, double, double>((x, y) => x * y);
Func<double, double, double> div = (x, y) => x / y;
var currieDiv = div.Curry();
curriedMulti(10)(20);
curriedMutli2(5)(10);
currieDiv(2)(2);
Wygląda to dziwnie, ale ma to sens.
Wywoływanie funkcji tylko częściowo
Zadajmy teraz sobie najważniejsze pytanie. Po co to w ogóle jest nam to potrzebne?
Powód jest prosty. Mając taki format funkcji jesteśmy w stanie dzielić aplikacje. Sprawić, że dana funkcja jest wykonana tylko z pewnymi kawałkami funkcji, a później jest ona umieszczona do zmiennej.
Spójrz na przykład poniżej.
Func<int, Func<int, int>> subC = x => y => x - y;
var sub11 = subC(11);
Mam więc podzieloną funkcję i wywołałem ją tylko częściowo, potrzebuję nadal drugiego parametru, aby obliczyć coś do końca. Ten stan rzeczy został zapisany do zmiennej sub11.
Funkcja wewnątrz tej zmiennej może być wywołana jak każda inna funkcja.
int result = sub11(5);
Stworzyłem więc nową funkcję z już istniejącej. Moja nowa funkcja zawsze będzie przyjmować pierwszy parametr jako 11.
Po co to wszystko? Funkcja odejmowania zawsze potrzebuje dwóch parametrów. Co, jeśli jednak jeden parametr zawsze jest taki sam. Co, jeśli mam inną funkcję i cały zbiór parametrów nie ulega zmianie.
Za każdym razem, gdybym wywoływał tę funkcję musiałbym przekazywać te same parametry, bez względu na to, czy one się powtarzają, czy nie. Jest to elastyczne, ale z drugiej strony zawsze muszę podawać wszystkie parametry.
Dlaczego więc nie mieć funkcji, która reprezentuje już podane częściowo parametry.
Gdybyśmy programowali obiektowo następujący problem moglibyśmy rozwiązać tak. Brakowałoby jednak w tym dynamizmu, bo w kodzie musielibyśmy napisać tonę przypadków dla gotowych domyślnych parametrów.
Nie zmienia to jednak faktu, że czasem dla małej ilości domyślnych parametrów piszemy właśnie taki kod.
int Sub(int x, int y)
{
return x - y;
}
int Sub11(int y)
{
return Sub(11, y);
}
Kod taki istnieje, ponieważ chcemy czasem skorzystać z istniejących już mechanizmów i mieć na nie wpływ w zależności od tego ile parametrów niedomyślnych podaliśmy.
Przykładowo mam metodę do tworzenia postów na blogu. Istnieją jednak przypadki, w których chcę stworzyć posty z gotową już listą komentarzy, dlatego więc mam metodę z tym parametrem.
Zazwyczaj jednak chcę utworzyć tylko wpis i komentarze mnie nie obchodzą. Czy potrzebuję dwóch metod? Czy może jednej?
public static bool CreatePost(Post post)
{
return CreatePost(post, new List<Comment>());
}
public static bool CreatePost(Post post, List<Comment> coment)
{
return true;
}
Ten problem rozwiązuję więc tak – korzystając już z gotowego kodu do tworzenia postów. Gdy chcę utworzyć tylko wpis przesyłam domyślnie pustą listę komentarzy. Jak widać rozbiór parametrów występuje też w świecie obiektowym.
Oczywiście jednak nie możesz stworzyć dodatkowej metody według zamysłu obiektowego, jeśli nie masz jej w kodzie źródłowym. Co więcej, nie zawsze napisanie w kodzie źródłowym dodatkowej metody rozwiązałoby problem. Kod tej klasy może być nie mój i zmiana kodu może tworzyć coś gorszego, bo łamię zasadę projektową otwarte-zamknięte.
W takich wypadkach wydaje się, że bezpiecznie jest napisać metody rozszerzeniowe LINQ. Czy jednak tak jest? Pisanie kolejnej metody co prawda nie w klasie, ale poza nią, tylko do pewnego algorytmu ma sens, czy nie?
Programowanie funkcjonalne rozwiązuje ten problem trochę inaczej. Jak zauważyłeś nie musiałem w kodzie źródłowym tworzyć nowej metody, a raczej funkcji.
Currying pozwala mi wywoływać metody, z jakimi parametrami mi się chce. Bez względu na to, czy pracuję ze swoimi funkcjami, czy napisanymi przez kogoś innego. Nie zaśmiecam żadnych
Kolejność wywołań ma znaczenie i to tworzy czasami problem
Wydaje się, że jest to największa wada tej techniki. Spójrz jeszcze raz na funkcję odejmującą.
Func<int, Func<int, int>> subC2 = x => y => x - y;
var sub77 = subC2(77);
Jak widać kolejność podawania parametrów w tym wypadku ma znaczenie. Co jednak mam zrobić, gdy chcę podać następny parametry, czyli Y, a nie X.
Nic nie stoi na przeszkodzie, aby napisać funkcję po Currying inaczej.
Func<int, Func<int, int>> subC2 = x => y => x - y;
var sub77 = subC2(77);
Func<int, Func<int, int>> subC3 = y => x => x - y;
var sub88 = subC3(88);
var r1 = sub77(11);
var r2 = sub88(11);
Co, jednak jeśli mam już taką funkcję i chcę zmienić kolejność?
Można napisać kolejną funkcję transformującą, która zamieni kolejność paramentów i zwróci nową funkcję.
Func<int, Func<int, int>> sub3 = x => y => x - y; Func<int, Func<int, int>> reorderedSub = y => x => sub3(x)(y);
Niestety tego procesu nie można zautomatyzować. Nie ma też gotowych funkcji, które by wykonały za ciebie zamianę parametrów.
public static class Swpa
{
public static Func<int, Func<int, int>> Swap
(this Func<int, Func<int, int>> fun)
{
return y => x => fun(x)(y);
}
}
class Program
{
static void Main(string[] args)
{
Func<int, Func<int, int>> sub4 = x => y => x - y;
var r4 = sub4.Swap()(11)(77);
Jest to problem, ale można go rozwiązać do pewnego stopnia.
W następnym wpisie przyjrzymy się jak ten cały Currying można wykorzystać w kontekście klasy.