ClousersCzęść NR.4

Aby język programowania mógł działać z Funkcjami wyższego rzędu musi rozwiązać problem z zasięgiem zmiennych. O co chodzi?

Funkcja wyższego rzędu jest to funkcja, która przyjmuje jako parametry kolejne funkcje. Co się jednak dzieje z parametrami zaszytymi w wewnętrznych parametrach funkcji?

Kiedy funkcje są przekazywane jako parametry, zmienne, wartości zwracane - wtedy kompilator używa domknięcia, aby rozszerzyć zasięg zmiennych tak, by były one dostępne wtedy, gdy są potrzebne.

W tym wpisie spojrzymy na problem z zasięgiem zmiennych i na domknięcia, ale najpierw…zobaczymy jak w C# dynamicznie tworzy się funkcję.

Tworzenie dynamicznie funkcji

Większość funkcjonalności w C# związana z tworzeniem funkcji bądź jak wolisz metod istnieje już od jakiegoś czasu. Wiele innych języków programowania posiada też właściwości przekazywania zmiennych, które referują się do funkcji.

W języku C mamy wskaźniki do funkcji, które przetrzymują referencję do funkcji.

W C# 1.0 delegaty nie rozszerzają tego pomysłu daleko. Na tym etapie pomysł w C# polegał na tym, by zmienne przechowujące funkcję były szczególnie oznaczone słowem kluczowym delegate.

W C# 2.0 pojawiła się ta poważna różnica pomiędzy językiem C a C#. Od C# 2.0 istnieje możliwość utworzenia funkcji anonimowej.

Zapewne się zastanawiasz, dlaczego jest to takie ważne? Otóż jest to fundament, który sprawia, że możemy tworzyć funkcje dynamicznie. Co więcej, funkcje te możemy tworzyć poza ich zasięgiem.

private static double ReturnValueGetDivision()
{
    var div = GetDivision();
    var result = div(10, 20);
    return result;
}

static Func<int, int, double> GetDivision()
{
    return (x, y) => x / y;
}

Jeśli masz problem ze zrozumieniem delegaty Func zapraszam na ten wpis.

Powyższy przykład nie rozwiązuje tak naprawdę żadnego problemu, nawet gorzej komplikuje tylko kod, bo ta sama składnia może być napisana dużo łatwiej.

Sprawy jednak stają się interesujące, gdy stworzymy funkcję dynamicznie a nie wklejamy na stałe pewne dane. By to było możliwe język programowania musi wspierać domknięcia.

Problem z zasięgiem i języki  funkcyjne

W C# jak dobrze pamiętasz zmienne żyją wewnątrz metod, a metody żyją wewnątrz klas lub w samych klasach. Oczywiście zmienne wewnątrz metody mogą być ograniczone przez bloki kodu jak IF, Else lub Try Catch.

Wszystkie zmienne w C# żyją w określonym bloku kodu. W bloku kodu, w którym zostały one zadeklarowane. Zmienna więc może stać się lokalną zmienną dla metody, klasy lub w pewnym sensie stać się zmienną globalną, jeśli jest statyczna.

Zmienne, jak sama nazwa wskazuje zmieniają się. Programowanie funkcyjne nie lubi pomysłu polegającego na zmianie wartości w programie. Języki funkcyjne mają inną filozofię w tej sprawie. Im bardziej zmienna jest publiczna i dostępna, tym większy jest problem.

W języku funkcjonalnym powinniśmy więc w ogóle nie zmieniać zmiennych i przetrzymywać wszystko w jak najmniejszym zasięgu.

Czysta funkcja spodziewa się pracować tylko ze zmiennymi lokalnymi w swoim bloku kodu i nic nie wychodzi poza nią.

Nic poza blokiem funkcji nie ma dostępu do tych zmiennych. Nic nie może ulec zmianie w inny sposób.

Poniższy kod pokazuje odwrotność tego pomysłu.

class Example
{
    public int SubstractA { get; set; }

    public int SubstractB { get; set; }

    public int AddA { get; set; }

    public int Multipla { get; set; }

    public int DivisionA { get; set; }

    public int DivisionB { get; set; }

    public void Do(int c, int d)
    {
        SubstractA = c - d;

        SubstractB = d - c;

        AddA = c + d;

        Multipla = c * d;

        DivisionA = c / d;

        DivisionB = d / c;
    }
}

Wielu programistów na samą myśl o tym pomyśle zastanawia się, czy to w ogóle jest możliwe, aby funkcja korzystała tylko ze zmiennych w swoim bloku i nie wystawiała ich na zewnątrz.

Zastanów się jednak dlaczego na przestrzeni tworzenia różnych platform i języków programowania stwierdzono, że zmienne globalne takie, jakie daje język C lub Pascal były złym pomysłem.

Posiadanie zmiennych globalnych tego rodzaju tworzy szanse kolizji i problem w zarządzaniu kodem, ponieważ trzeba zgadywać, co ta zmienna przechowuje dokładnie w pewnym momencie trwania aplikacji, który jest dla nas istotny.

Teraz uważaj, bo ta idea zostanie rozszerzone jeszcze bardziej.

Posiadanie pól w klasach, które mogą zmieniać się wewnątrz metod w klasie nie daje tak naprawdę dużej różnicy problematycznie jeśli chodzi o zmienne globalne.

Zasięg jest inny dużo mniejszy, ale w dużej klasie wciąż musisz się zastanawiać, co dokładnie przechowuje twoje pole w trakcie działania aplikacji.

Problem kolizji oczywiście jest mniejszy, ale problem według niektórych filozofów programowania istnieje nadal.

class Example
{
    private int substractA;
    private int substractB;
    private int addA;
    private int multipla;
    private int divisionA;
    private int divisionB;

    public Example(int a,int b, int c,
		 int d, int e, int f)
    {
        substractA = a;
        substractB = b;
        addA = c;
        multipla = d;
        divisionA = e;
        divisionB = f;
    }

    public void Do2(int c, int d, int e)
    {
        substractA = c - d - e;

        substractB = d - c - e;

        addA = c + d + e;

        multipla = c * d * e;

        divisionA = c / d / e;

        divisionB = e/ d / c;
    }

    public int ReturnV(int a)
    {
        switch (a)
        {
            case 1:
                return substractA;
            case 2:
                return addA;
            case 3:
                return multipla;
            case 4:
                return divisionA;
            case 5:
                return divisionB;
            default:
                return substractB;
        }
    }

    public Example Return()
    {
        return new Example(substractA, substractB, 
		addA, multipla, divisionA, divisionB);
    }

    public void Do(int c, int d)
    {
        substractA = c - d;

        substractB = d - c;

        addA = c + d;

        multipla = c * d;

        divisionA = c / d;

        divisionB = d / c;
    }
}

Dla nich zapewne klasy wyglądają w taki sposób.

Niestety w C# czasami ograniczenie wartości tylko do bloku funkcji jest niemożliwe.

Co, jeśli twoja aplikacja w trakcie startu ustawia jakieś wartości po to, by później można było z nich skorzystać i to skorzystanie odbywa w wielu miejscach w aplikacji.

Zawsze jednak na pomoc mogą przyjść domknięcia.

Domknięcia jak one działają

Jak działają domknięcia? Spójrz na poniższy kod.

static void Closures()
{
    Console.WriteLine(GetFunction()(300));
}


static Func<int, int> GetFunction()
{
    int val = 100;
    Func<int, int> internalAdd = x => x + val;

    Console.WriteLine(internalAdd(10));

    val = 300;

    Console.WriteLine(internalAdd(10));

    return internalAdd;
}

Śledzą ścieżkę wywołania funkcji widzisz, że Closures wywołuje GetFunction. Przy wywołaniu GetFunction pojawiają się drugie nawiasy, a z nimi parametr 300.

Dlaczego tak jest? GetFunction zwraca funkcję, a ta zostaje natychmiastowo wywołana.

Wewnątrz GetFunction znajduje się zmienna val, która jest lokalnie używana jako parametr do funkcji internalAdd.

Co zwróci konsola przy pierwszym wywołaniu funkcji. 100 + 10 = 110

Później val ma inną wartość 300 i wywołana ona ponownie. 300 + 10 = 310

To zachowanie jest oczywiście prawidłowe, ponieważ zmiany odbywają się wewnątrz lokalnej funkcji. Zmiana zmiennej jest respektowana przez wskaźnik do metody internalAdd.

Co jednak stanie się dalej.

Wychodzimy z lokalnej funkcji, co jednak się dzieje z lokalną zmienną VAL.

VAL powinna być zmienną żyjącą na stosie i po wykonaniu metody powinna ona zniknąć. Czy jednak tak jest?

To jest właśnie główny cel domknięcia, aby zapobiegać wartościom wyskakiwania poza zasięg, gdyż kompilator na tym etapie może stwierdzić, że ta operacja rozwaliłaby program.

Jak więc działają domknięcia? Kompilator widzi anonimową funkcję wewnątrz GetFunction i to, że ona referuję się do zmiennej VAL, która będzie poza zasięgiem.

Skoro funkcje mogą być wysyłane jak dane w C# istnieje szansa, że funkcja będzie żyła dłużej niż zmienne wewnątrz tej funkcji jak VAL.

Jeżeli więc zwrócona funkcja jest później wywołana w zupełnie innym miejscu w programie, to wtedy aplikacja by się wywaliła. Co wiec robi kompilator.

Postanawia przetrzymać tą zmienną w bezpiecznym miejscu, w pamięci niezależnym od klasy, ale od funkcji, w której on się znajduje.

Kompilator tworzy anonimową klasę z tą zmienną i ta anonimowa klasa jest tworzona wewnątrz tej anonimowej funkcji.

Ostatecznie zmienna lokalna VAL nie jest już zmienną lokalną tylko polem anonimowej klasy utworzonej na potrzeby tej funkcji.

InternalAdd teraz ma referencje do funkcji, która ma referencję do instancji anonimowej klasy z tą zmienną.

Wracając do pomysłu tworzenia funkcji dynamicznie. Istnieje możliwość utworzenia nowej funkcji z niczego, której zachowanie jest zależne od parametrów.

private static void DynamicAdd()
{
    var m5 = GetMultiX(5);
    var m10 = GetMultiX(10);
    Console.WriteLine(m5(10));
    Console.WriteLine(m10(10));
}

private static Func<int, int> GetMultiX(int staticVal)
{
    return x => staticVal * x;
}

Kod ten przy m5 zwróci 50, ponieważ 5 * 10.

Przy m10 kod zwróci 100, ponieważ 10 * 10.

Ustawiam pole jednej zmiennej do mojej utworzonej dynamicznie funkcji. Później do mnożenia przy wywołaniu dodaję drugi parametr.

Jak widać parametry przetrzymującej referencje m5 i m10 różnią się tym, że mają w sobie inne klasy anonimowe z inną wartością pola staticVal.

Te mechanizmy są kluczową podstawą do pewnych technik funkcyjnych, które omówię później.

Nawet ten prosty przykład pokazuje, że jest to technika różniąca się od klasycznego podejścia obiektowego jak przeciążanie metod.

W przeciwieństwie jednak do przeciążania metod ta technika może zostać utworzona dynamicznie w trakcie działania programu.

Pewne algorytmy dzięki tym technikom są łatwiejsze do utworzenia. Ta technika ułatwia też czytelność kodu bardziej niż podejście obiektowe z klasami i poziomami dostępu.

Praktyczne użycie domknięć znajduje się we wzorcu async używanym w .NET.

W tym przykładzie używam oddzielnej klasy do przechowywania informacji pomiędzy wywołanymi asynchronicznymi.

Oczywiście tworzy to problem, bo to oznacza, że muszę dla każdej takiej operacji tworzyć własną klasę.

class Program
{

    public class InfoHolder
    {
        public WebRequest Request { get; set; }
        public string ClientId { get; set; }
        public string Query { get; set; }
    }
    private static void QueryVersion1(string clientId, string query)
    {
        var request = WebRequest.Create
		("http://www.google.com/search?q=" + query);
        request.BeginGetResponse(QueryCallback1,
        new InfoHolder
        {
            Request = request,
            ClientId = clientId,
            Query = query
        });
    }

    private static void QueryCallback1(IAsyncResult ar)
    {
        var state = ar.AsyncState as InfoHolder;
        if (state != null)
        {
            var response = state.Request.EndGetResponse(ar);
            Console.WriteLine(state.ClientId);
            Console.WriteLine(state.Query);
            Console.WriteLine(state.Request.RequestUri);
            StreamReader reader = 
		new StreamReader(response.GetResponseStream());
            string text = reader.ReadToEnd();
            Console.WriteLine(text.Remove(0,text.Length - 500));
            response.Close();
        }

    }

    public static void Main(string[] args)
    {
        QueryVersion1("1", "cezary walenciuk");
        Console.ReadKey();
    }
}

Alternatywne rozwiązanie polega na użyciu domknięć, które za mnie utworzą klasy anonimowe do przechowywania odpowiednich pól.

private static void QueryVersion2(string clientId, string query)
{
    var request = WebRequest.Create
		("http://www.google.com/search?q=" + query);
    request.BeginGetResponse(
    (IAsyncResult ar) => {
        var response = request.EndGetResponse(ar);
        Console.WriteLine(clientId);
        Console.WriteLine(query);
        Console.WriteLine(request.RequestUri);
        StreamReader reader = new StreamReader(response.GetResponseStream());
        string text = reader.ReadToEnd();
        Console.WriteLine(text.Remove(0, text.Length - 500));
        response.Close();
    }, null);
}

Kod taki jest bardziej czytelny.

Domknięcia są istotnym mechanizmem potrzebnym dla języków funkcjonalnych. W tym wpisie omówiliśmy, jak kompilator je obsługuje i ta wiedza będzie nam potrzebna w dalszych wpisach.