CodeGym /Kursy /C# SELF /Wprowadzenie do programowania funkcyjnego

Wprowadzenie do programowania funkcyjnego

C# SELF
Poziom 51 , Lekcja 0
Dostępny

1. Wprowadzenie

Programowanie funkcyjne (PF) — to paradygmat programowania, w którym podstawowy budulec to nie obiekt i nie procedura/metoda, lecz funkcja w sensie matematycznym. W PF główny nacisk kładzie się na opis "co obliczać", a nie "jak obliczać".

Spotykaliście się już z poszczególnymi pomysłami PF, pracując z wyrażeniami lambda i LINQ. Ale gdzie jest różnica? W praktyce: OOP opisuje obiekty i ich interakcje, programowanie proceduralne — zestaw kroków, a PF — kompozycję funkcji, przekazywanie zachowania jako wartości, rezygnację ze zmiany stanu (immutability) i brak efektów ubocznych.

Po co w ogóle nowa paradygmata?

  • Czystszy, przewidywalny i łatwiejszy do testowania kod.
  • Uproszczone wsparcie dla wielowątkowości ("brak stanu — brak problemu").
  • Zwięzłość i ekspresywność (im mniej kodu — tym mniej błędów).
  • Wysokopoziomowe, łatwo wielokrotnego użytku abstrakcje.

AnaloGia

Wyobraź sobie, że restauracja dostała zamówienie: "przygotować omlet". Kucharz imperatywny wykonuje listę instrukcji: wziąć jajka, rozbić, ubić, usmażyć. Kucharz funkcyjny mówi: res = omlet(jajka) — operuje funkcjami, abstrahując od wewnętrznego stanu kuchni (no, prawie).

W C# możemy używać obu podejść. To sprawia, że język jest bardzo elastyczny i potężny — szczególnie w realnych projektach.

Kluczowe koncepcje PF

1. Funkcje wyższego rzędu

Funkcje można przekazywać jako parametry, zwracać z innych funkcji i przechowywać w zmiennych. Robiliście to już z wyrażeniami lambda i delegatami. W PF takie "funkcje nad funkcjami" są podstawą wszystkiego.

2. Czyste funkcje

Funkcja jest "czysta", jeśli jej wynik zależy tylko od parametrów i nie zmienia niczego poza sobą (brak efektów ubocznych). Dwa identyczne wywołania z tymi samymi argumentami zwrócą ten sam wynik.

3. Niezmienność (Immutability)

Dane nie mutują "w miejscu": nowy stan to nowy obiekt. To bardzo upraszcza rozumowanie o programie i pomaga w wielowątkowości.

4. Brak efektów ubocznych

Funkcja nic nie zapisuje do pliku, nie zmienia zmiennych globalnych, nie rysuje na ekranie — po prostu zwraca wynik. W praktyce efekty uboczne są nieuniknione, ale staramy się je izolować na brzegach systemu.

5. Kompozycja funkcji

Jedną funkcję można zbudować z innych, jak z klocków. Na przykład: przefiltrować liczby dodatnie, wziąć ich kwadraty i zsumować. Każda operacja — osobna funkcja i łatwo je łączyć (WhereSelectSum).

2. PF w C#: od teorii do praktyki

C# — język multiparadygmatyczny: świetnie wspiera OOP, podejście proceduralne i potężny styl funkcyjny (z lambdami, delegatami, metodami rozszerzeń i LINQ).

Rozpatrzmy na przykładzie naszej aplikacji edukacyjnej

Wyobraźmy sobie, że rozwijamy program do pracy z listą liczb i stringów. Naszym zadaniem jest stosować do tych danych różne operacje w stylu funkcyjnym.

Przykład 1: Użycie funkcji wyższego rzędu


// Zastosowuje akcję do wszystkich elementów listy
public static void ForEach<T>(List<T> items, Action<T> action)
{
    foreach (var item in items)
    {
        action(item);
    }
}

Użycie:


var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // Funkcja-parametr

Widzisz? Funkcję można "składać" w zmienną lub przekazywać jak zwykłą wartość — zupełnie jak jabłko w kuchni!

Przykład 2: Czysta funkcja

Funkcja, która nie zmienia stanu programu i zależy tylko od wejścia:


int MultiplyByTwo(int x)
{
    return x * 2;
}
  • Nie zależy od niczego zewnętrznego.
  • Nic poza sobą nie zmienia.
  • Dla x = 5 zawsze zwróci 10.

Porównaj z funkcją, która używa i zmienia zmienną globalną:


int total = 0;
int AddToTotal(int x)
{
    total += x;
    return total;
}

To już nie jest czysta funkcja — wynik zależy od zewnętrznego stanu i on go modyfikuje.

Przykład 3: Niezmienność danych

Zamiast zmieniać dane wejściowe tworzymy nowe:


List<int> AddOneToEach(List<int> numbers)
{
    return numbers.Select(n => n + 1).ToList();
}

Lista wejściowa w ogóle się nie zmienia. W programach wielowątkowych to szczególnie wygodne: mniej blokad i wyścigów danych.

Przykład 4: Kompozycja funkcji

Uzyskać sumę kwadratów wszystkich parzystych liczb:


int SumOfEvenSquares(List<int> numbers)
{
    return numbers
        .Where(n => n % 2 == 0)     // Zostawić tylko parzyste
        .Select(n => n * n)         // Podnieść do kwadratu
        .Sum();                     // Zsumować
}

Czytelne i deklaratywne: każda operacja — osobna funkcja.

3. Przydatne niuanse

PF, LINQ i C#

LINQ — to prawie "PF w praktyce" dla kolekcji: używasz funkcji wyższego rzędu (Where, Select itd.), otrzymujesz nowe sekwencje bez mutowania źródła, a każde przekształcenie to osobne wyrażenie. Wynik to IEnumerable<T>, które opisuje, co otrzymać, a nie jak iterować.

Tabela analogii

Imperatywnie (proceduralnie/OOP) Funkcyjnie (LINQ/styl PF)
foreach (var x in xs) ...
xs.Select(...)
„Mutować” kolekcję Uzyskać nową kolekcję
Stan (total += x) Czyste funkcje (xs.Sum())
Opisywać jako: „zrób to” Opisywać: „co chcemy otrzymać”

PF vs OOP: dwa światy — jeden C#

To nie konkurencyjne obozy. W realnych projektach C# łączy się je: model domeny wygodnie budować na klasach (OOP), a przetwarzanie kolekcji, agregacje i transformacje — w stylu funkcyjnym przez LINQ, lambdy i metody rozszerzeń.

Twoja wiedza o delegatach jest bezpośrednio użyteczna: Func<T, TResult>, Predicate<T>, Action<T> — to typowe cegiełki stylu PF.

Uniwersalna funkcja filtrująca:


List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (predicate(item))
            result.Add(item);
    }
    return result;
}

Wywołania:


var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);

Zamiast wielu podobnych metod z różnymi warunkami — jedna uniwersalna funkcja.

Dlaczego pracodawcom i na rozmowach kwalifikacyjnych przydają się PF-deweloperzy?

  • PF pomaga testować małe bloki kodu bez uruchamiania całego systemu.
  • Łatwiej utrzymywać logikę: mniej stanów — mniej źródeł błędów.
  • Łatwiej pisać kod równoległy i asynchroniczny — brak stanu globalnego, mniej wyścigów danych.

Jak nie przesadzać?

Tak, PF jest potężne. Ale C# nie jest językiem czysto funkcyjnym i nie wszystkie zadania wymagają idealnej czystości. Nie bój się lokalnych zmiennych i rozsądnej mutacji tam, gdzie to ma sens. Najważniejsze to czytelność, przewidywalność i testowalność. Elementy PF to narzędzie, a nie religia.

4. Typowe błędy początkujących

Bardzo łatwo trafić na pozory kodu funkcyjnego, który w praktyce nie jest funkcyjny.

Na przykład: funkcja zwraca nową kolekcję, ale w środku po drodze mutuje listę źródłową — to łamie zasadę niezmienności i psuje oczekiwania wywołującego kod.

Inny przykład: wyrażenie lambda odwołuje się do zmiennej zewnętrznej i ją zmienia. W paradygmacie funkcyjnym uważa się to za efekt uboczny i sprawia, że zachowanie kodu jest mniej przewidywalne.

Kompilator C# cię nie powstrzyma: język pozwala jedno i drugie. Dlatego w praktykach PF ważne jest pilnowanie, żeby funkcja "żyła sama za siebie", nic nie zmieniała na zewnątrz i nie czytała nic poza swoimi argumentami.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION