Jak używać programowania funkcyjnego w TypeScript?

Od jakichś dwóch lat w środowisku związanym z JavaScriptem trwa dyskusja o programowaniu funkcyjnym. Pozwala ono tworzyć lepsze oprogramowanie bez projektowania skomplikowanej struktury klas. Dziś opiszę jak używać złożenia funkcji w TypeScripcie i Lodashu.

Kod można znaleźć na Githubie.


Co to jest złożenie funkcji?


Złożenie funkcji (ang. function composition) polega na połączeniu dwóch lub więcej funkcji żeby utworzyć nową, bardziej złożoną. Zdezorientowani? Spokojnie, poniższy przykład pomoże to zrozumieć:

const f = function (a) { return a + 1 };

const g = function (b) { return b * b };

const x = 2;

const result = f(g(x)); // => 5

Połączyłem tu dwie funkcje — funkcję f i funkcję g. Funkcja f dodaje 1 do parametru a, natomiast funkcja g mnoży parametr b razy b. Wynik wynosi 5.


Przyjrzyjmy się temu:

  1. Stała x równa się 2.
  2. Stała x staje się argumentem funkcji g.
  3. Funkcja g zwraca 4.
  4. Wynik funkcji g(4) staje się argumentem funkcji f.
  5. funkcja f zwraca 5.

Żadna filozofia, tylko czy to się może do czegoś przydać? Przecież prościej byłoby zrobić to w jednej funkcji. Może i tak, ale pomyślmy o konkretnych sytuacjach, w których możemy tego użyć.


Przykład z życia wzięty: formatowanie walutowe

Budowałem prostą aplikację do publikowania ofert pracy dla programistów. Chodziło między innymi o to, żeby przy każdej ofercie wyświetlić widełki płacowe. Wszystkie pensje były przechowywane jako centy i trzeba było je zamienić na taki format:

from: 6000000

to:   60,000.00 USD

Wydaje się łatwe, ale praca z tekstem jest trudna. Prawie wszyscy developerzy jej nie znoszą. Godzinami piszemy wyrażenia regularne i pracujemy z unicodem. Kiedy mam sformatować tekst, zawsze najpierw gugluję rozwiązanie. Po odsianiu wszystkich bibliotek (stanowczo zbyt wielu jak na moje potrzeby) i wszystkich beznadziejnych kawałków kodu niewiele zostaje.

Zdecydowałem, że muszę zrobić własny formatter.


Jak to zrobić?


Zanim zaczniemy pisać kod, przyjrzyjmy się dokładnie pomysłowi:

  1. Rozdziel dolary i centy.
  2. Sformatuj dolary - dodawanie separatorów tysięcy wcale nie jest takie łatwe.
  3. Sformatuj centy — to łatwe, w zasadzie ootb.
  4. Połącz dolary i centy z separatorem.

Teraz już trochę jaśniej. Ostatnia rzecz, którą musimy wziąć pod uwagę, to dodanie separatorów tysięcy do dolarów. Rozważmy następujący algorytm:

  1. Odwróć ciąg.
  2. Rozdziel ciąg co 3 znaki, żeby stworzyć listę.
  3. Połącz wszystkie elementy z listy dodając separator tysięcy między nimi.
  4. Odwróć ciąg.

Wszystkie te kroki można przetłumaczyć na następujący pseudo kod:

1. "60000"        => "00006"
2. "00006"        => ["000", "06"]
3. ["000", "06"]  => "000.06"
4. "000.06"       => "60.000"

A tak ten algorytm tłumaczy się na skomponowane funkcje:

import { curryRight, flow, join } from "lodash";

import split from "./split";
import reverse from "./reverse";

/**
 * Thousand Regular Expression
 */
const thousandRegExp:RegExp = /[0-9]{1,3}/g;

/**
 * @example splitCurry(RegExp)(string)
 */
const splitCurry:Function = curryRight(split);

/**
 * @example joinCurry(separator)(string)
 */
const joinCurry:Function = curryRight(join);

export default function thousandSeparator(separator:string):Function {
    return flow([
        reverse,                    
        splitCurry(thousandRegExp),  
        joinCurry(separator),
        reverse,
    ]);
}​

W pierwszej linii można zauważyć, że korzystałem z biblioteki Lodash - zawiera wiele narzędzi, które ułatwiają programowanie funkcyjne. Przeanalizujmy kod od 22 linii:

return flow([
  reverse,
  splitCurry(match),
  joinCurry(separator),
  reverse
]);

Funkcja flow jest kompozytorem funkcji. Wysyła wynik funkcji reverse na wejście splitCurry i tak dalej. Dzięki temu powstaje zupełnie nowa funkcja. Pamiętasz tamten algorytm separacji tysięcy? No właśnie!

Widać, że postfixowałem nazwy funkcji split i join z "Curry" i wywołałem je. Ta technika nazywa się currying.

Co to jest currying?

Currying to proces tłumaczenia funkcji o wielu argumentach na funkcję o jednym argumencie. Funkcja o jednym argumencie zwraca inną funkcję jeśli argumenty są nadal potrzebne.


Trudne? Spójrzcie na ten przykład:

/**
 * @param {string} string 
 * @param {RegExp} pattern 
 */
export default function split(string:string, pattern: RegExp):Array<string> {
    return string.match(pattern) || [];
}​

Funkcja split wymaga dwóch argumentów — wzorca ciągu i separacji. Funkcja musi wiedzieć, jak podzielić tekst. W takim przypadku nie możemy komponować tej funkcji, bo potrzebuje ona innych argumentów niż pozostałe. I tutaj przyda nam się currying.

import { curry } from "lodash";

import split from "./split";

const splitCurry = curry(split);

splitCurry("000001")(/[0-9]{1,3}/g); // => ["000", "001"]

Teraz funkcja splitCurry jest zgodna z funkcją reverse. Obie z nich potrzebują jednego argumentu. Niestety, nie wywołaliśmy jeszcze funkcji splitCurry z wzorcem separacji. No tak to nie zadziała.

A jeśli odwrócimy kolejność argumentów w naszym curry?

import { curryRight } from "lodash";

import split from "./split";

const splitCurry = curry(split);

splitCurry(/[0-9]{1,3}/g)("000001"); // => ["000", "001"]

Teraz kod może działać, bo możemy użyć curry jako funkcji fabryki. Popatrzmy znowu na kod:

return flow([

reverse,

splitCurry(match),

joinCurry(separator),

reverse

]);

Wszystkie te funkcje przyjmują pojedynczy argument (ciąg), więc możemy skomponować je razem, a następnie użyć ich jako samodzielnej funkcji. Chwileczkę…


Co to są funkcje fabryki?

Funkcje fabryki to takie, które tworzą nowy obiekt. W tym przypadku nowym obiektem jest funkcja. Rozważmy ponownie nasz separator tysięcy. Teoretycznie możemy używać cały czas tej samej funkcji. Niestety, niektóre kraje oddzielają tysiące przecinkami, inne kropkami. Oczywiście mogłem sparametryzować separator, ale zdecydowałem się na wykorzystanie fabryki.

const formatUS = thousandSeparator(',');

const formatEU = thousandSeparator('.');

formatUS(10000); // 10,000

formatEU(10000); // 10.000


Podsumowanie

W zeszłym roku poświęciłem trochę czasu na odświeżenie wiedzy z poprzednich studiów, żeby móc z niej korzystać na co dzień. W tym artykule nie zagłębiałem się w prawo monad czy monoidy — nie chciałem komplikować. Temat jest szeroki i mocno związany z informatyką. Chciałem przybliżyć Wam myślenie funkcyjne opisując wszystko tak krótko, jak to możliwe.


________
Artykuł został opublikowany tutaj. A tutaj znajdziecie blog autora.