Programowanie funkcyjne w Twojej przeglądarce

2

Witaj w czwartej odsłonie cyklu! Pierwsze trzy odcinki poświęciłem na przedstawienie Ci podstaw programowania funkcyjnego w C#. Czas na mały plot twist – tym razem zajmiemy się JavaScriptem.

Dlaczego? Po pierwsze dlatego, że ten najpopularniejszy na świecie język programowania jest funkcyjny (wg raportu Stack Overflow)! Wzgardzony przez wielu i często traktowany jako niepoważny JavaScript w rzeczywistości zawiera wiele elementów wspierających programowanie funkcyjne. Za chwilę zobaczysz, że niektóre koncepcje poznane w poprzednich osłonach cyklu wyrażają się w JavaScripcie w sposób dużo bardziej naturalny niż w C#.

Po drugie chcę Ci pokazać, że idee w programowaniu funkcyjnym są uniwersalne i tak naprawdę nie ma znaczenia, jakim językiem się posługujemy.

Po ostatnie to właśnie w świecie frontendu programowanie funkcyjne przeżywa swój niespodziewany renesans. Nowoczesne biblioteki JavaScriptowe takie jak React, Redux czy RxJS pełne są odwołań do tego paradygmatu programowania.

Zanim przejdziemy do rzeczy, mam dla Ciebie niespodziankę – bonus!

Po tym wstępie zapraszam do lektury!

Funkcje obywatelami pierwszej kategorii

Pierwsze, najważniejsze kryterium, które musi spełniać język funkcyjny, to możliwość traktowania funkcji w taki sam sposób jak traktowane są dane. Jak w tej kwestii spisuje się JavaScript?

Z łatwością możemy przypisać funkcję do zmiennej (lub stałej):

const calculateTax = function(employee) {
  return employee.salary * 0.19;
}

Dzięki wprowadzonej w standardzie ES6 notacji arrow functions jest to jeszcze łatwiejsze (i przypomina lambda expressions z C#):

const calculateTax = employee => employee.salary * 0.19;

Mając taką możliwość, możemy bez problemu przekazać funkcję jako argument innej funkcji. Poniżej przykład wywołania metody wyższego rzędu map na obiekcie tablicy:

const squares = [1, 2, 3, 4, 5].map(x => x * x);

JavaScript posiada kilka takich wbudowanych metod tablicowych, które dają namiastkę możliwości oferowanych przez LINQ. Istnieją biblioteki znacznie rozszerzające zakres funkcji wyższego rzędu na kolekcjach, np. lodash.

Nie ma problemu ze zwracaniem funkcji z funkcji:

function getTaxMethod(employee) {
  if (employee.salary > 10000) {
    return e => 1900 + (e.salary - 10000) * 0.32;
  } else {
    return e => e.salary * 0.19;
  }
}

const calculateTax = getTaxMethod(employee);
console.log(calculateTax(employee));

Czyste funkcje

Trudno mówić o jakimś konkretnym wsparciu języka dla czystych funkcji, ale JavaScript posiada konstrukcje, które ułatwiają niemodyfikowanie obiektów i tablic. Jeśli nie wiesz, czym są czyste funkcje, to odsyłam do części pierwszej.

Pisząc czyste funkcje operujące na obiektach, nie wolno nam mutować podanego argumentu. Zamiast tego zwracamy nowy obiekt będący kopią argumentu rozbudowaną o pożądane modyfikacje.

Standard ES2018 języka wprowadza tzw. operator spread dla obiektów. Dzięki niemu możemy w łatwy sposób skopiować pola jednego obiektu do drugiego, przy okazji mając możliwość dodania nowych lub nadpisania starych pól:

function calculateTax(employee) {
  const getTax = getTaxMethod(employee);
  return { ...employee, tax: getTax(employee) };
}

Już w standardzie ES6 funkcjonował wariant operatora spread działający na tablicach. Dzięki niemu możemy z łatwością implementować niemutowalne operacje na tablicach:

function addItem(items, item) {
  return [ ...items, item ];
}

Pisanie takich funkcji może rodzić obawy o niepotrzebne mnożenie obiektów. Biblioteka adresuje ten problem, bazując na koncepcji structural sharing.

Currying i częściowa aplikacja

Jak pamiętamy z części drugiej, funkcje jednoargumentowe są kluczowe w kwestii funkcyjnej kompozycji.

W JavaScripcie możemy z łatwością zapisać funkcje w postaci curried. Jest to przyjemniejsze niż w C#, ponieważ nie jesteśmy zmuszeni wypisywać typów kolejnych argumentów oraz typu zwracanego przez funkcję.

Na ogół silne systemy typów dobrze współgrają z programowaniem funkcyjnym, jednak tylko jeśli mamy do dyspozycji inferencję typów (kompilator sam zgaduje, o jaki typ nam chodzi). W C# mamy słowo kluczowe var, ale typy argumentów funkcji oraz typ zwracany muszą być jawnie podane. W tej sytuacji dynamiczny JavaScript okazuje się dużo wygodniejszy.

const calculateTax = 
  taxRate => employee => taxRate * employee.salary;
  
const calculateTax19 = calculateTax(0.19);
const calculateTaxZero = calculateTax(0);

console.log(calculateTax19({ name: "Jan", salary: 100 })); 

Funkcyjna kompozycja

Ponownie brak sprawdzania typów ułatwia nam trochę życie. W JavaScripcie możemy w niezwykle elegancki sposób zapisać generyczną funkcję compose służącą do składania innych funkcji.

const compose2 = (f, g) => x => g(f(x));
const compose = (...args) => args.reduce(compose2);

Najpierw definiujemy funkcję compose2 służącą do składania dwóch funkcji. Następnie definiujemy funkcję compose. Za pomocą poznanego już operatora spread przypisujemy wszystkie argumenty funkcji (może ich być dowolna liczba) do tablicy args. Tablica ta będzie zawierać wszystkie funkcje, które mamy złożyć.

W kolejnym kroku wywołujemy na tablicy args metodę wyższego rzędu reduce. Zachowuje się ona w podobny sposób co Aggregate w LINQ. Jej magia polega na tym, że podając jej sposób na złożenie dwóch funkcji (compose2), otrzymamy złożenie wszystkich funkcji w tablicy args.

Funkcja compose jest często znana pod nazwą pipe. W takiej formie weszła do użytku w niezwykle popularnej ostatnie bibliotece RxJS wspomagającej programowanie reaktywne w JavaScript. Funkcja pipe służy w tym przypadku do składania transformacji, którym poddajemy obserwowalne strumienie zdarzeń.

number$.pipe(
    map(n => n * n),
    filter(n => n % 2 === 0)
);

Przykład ten jest o tyle ważny, że pokazuje, iż programowanie funkcyjne to nie jest jakieś akademickie bajanie, tylko źródło koncepcji, na których opiera się tworzenie nowoczesnych aplikacji webowych.

Monady

Po dłuższe wyjaśnienie tego, czym są monady, zapraszam do części trzeciej cyklu.

W JavaScripcie, ponownie, brak silnego systemu typów upraszcza tworzenie i użycie monad. Nie ma tutaj składni LINQ pozwalającej na zwięzły zapis monadycznego kodu. Zamiast tego można użyć generatorów – kolejnego elementu języka wprowadzonego w standardzie ES6.

Warto nadmienić, że monady są – często nieświadomie – używane przez wielu programistów JavaScript na co dzień. Otóż obiekt Promise reprezentujący asynchroniczne obliczenie (np. pobranie danych z serwera) jest niczym innym jak monadą, którą można składać za pomocą metody then. Koncepcja ta jest bardzo podobna do klasy Task z C#, z tym że w JavaScripcie Promise’y wykorzystywane są bardzo często.

Podsumowanie

Na tym kończę ten krótki przegląd funkcyjnych elementów JavaScriptu. Jak widzisz, język ten jak najbardziej nadaje się do pisania funkcyjnego kodu, a w wielu miejscach jest wygodniejszy niż C#.

Wspomniałem kilka razy, że brak silnego systemu typów jest w tym kontekście pomocny. Warto jednak pamiętać, że hardcore’owe języki funkcyjne, takie jak Haskell czy F#, mają bardzo silny system typów świetnie współgrający z funkcyjnością.

Co o tym sądzisz? Czy udało mi się pokazać Ci, że JavaScript to całkiem poważny język, w którym możesz także pisać funkcyjnie? Zapraszam do sekcji komentarzy oraz na kolejną część cyklu, z której dowiesz się, jak w JavaScripcie stworzyć pełnoprawną aplikację webową w sposób funkcyjny!

Pamiętaj o bonusie, do zgarnięcia poniżej :)

Share.

About Author

Jestem programistą full-stack, na co dzień piszącym w .NET oraz JavaScripcie. Pasjonuję się programowaniem funkcyjnym, a zwłaszcza jego zastosowaniom w tworzeniu nowoczesnych aplikacji webowych. Lubię dzielić się wiedzą na meet-upach, konferencjach oraz szkoleniach. Prowadzę bloga programistycznego.

2 Comments

  1. dpindral on

    Wiem, że to tylko przykład i dla teorii odnośnie samego programowania funkcyjnego większego znaczenia to nie ma, ale ta funkcja calculateTax z fragmentu o curryingu chyba nie do końca liczy to co chciałeś. Skoro to ma być podatek to drugi raz nie trzeba tego przemnażać przez wysokość wynagrodzenia ;)