Dziś przyjrzymy się kolejnemu elementowi, który łączy się z programowaniem funkcyjnym. W poprzednich wpisach dotyczących programowania funkcyjnego poruszyłem temat czystych funkcji oraz higher order functions. We wpisie na temat HOC wspomniałem o kompozycji funkcji. Dziś chciałbym nieco rozszerzyć ten temat i na podstawie przykładu, krok po kroku wyjaśnić na czym polega standardowe podejście oraz podejście przy wykorzystaniu kompozycji funkcji. Później też okaże się, dlaczego na blogu opublikowałem artykuły w takiej, a nie innej kolejności, bowiem wszystkie elementy są ze sobą powiązane.

Wyobraźmy sobie, że chcemy napisać metodę, która wykonuje jakieś operacje na stringu. Na przykład zamienia znaki na małe, usuwa wszystkie spacje i na końcu dodaje wykrzyknik.

W standardowym podejściu moglibyśmy stworzyć funkcję w stylu: modifyString(inputString), która dane operacje wykona wewnątrz tej funkcji i zwróci wynik końcowy.

Funkcja mogłaby wyglądać mniej więcej tak:

const modifyString = (inputString) => (
  inputString.replace(/\s+/g, '').toLowerCase() + '!'
);

modifyString("AbC dEf GhI"); // abcdefghi!

Pewnie można napisać funkcję na kilka innych sposobów, ładniej itd., natomiast nie o to w tym chodzi… 🙂

Jeżeli przyjrzymy się zawartości funkcji, to nie trudno wyodrębnić tutaj trzy proste kroki:

  • usunięcie spacji
  • zamiana znaków na małe
  • dodanie wykrzyknika na końcu

Możemy teraz zapisać każdą z tych operacji w postaci funkcji. Ważne, aby przy użyciu kompozycji i podejściu funkcyjnym napisać ją tak, aby funkcja była czysta (tutaj odsyłam do artykułu na temat czystych funkcji).

Funkcje wykonujące dane operacje zdefiniowaliśmy poniżej:

const toLower = (inputString) => (
    inputString.toLowerCase()
);

const removeSpace = (inputString) => (
  inputString.replace(/\s+/g, '')
);

const addExclamationMark = (inputString) => (
  inputString + '!'
);

Wykonanie funkcji w poniżej zdefiniowany sposób da nam ten sam wynik, który otrzymaliśmy, zapisując logikę w jednej metodzie modifyString():

modifyString("AbC dEf GhI"); // abcdefghi!
addExclamationMark(toLower(removeSpace("AbC dEf GhI"))); // abcdefghi!

Na co trzeba zwrócić uwagę stosując takie rozwiązanie?
Przede wszystkim na kolejność wykonywania operacji. W naszym przypadku, czy usuniemy spację na początku, czy na końcu, czy zmienimy litery na małe przed, czy po dodaniu wykrzyknika, nie ma w zasadzie różnicy, jeżeli chodzi o wynik końcowy… Natomiast niektóre operacje mogą tego wymagać.

Przykład:

const add2 = (number) => (
  number + 2
);

const multiply2 = (number) => (
  number * 2
);

add2(multiply2(2)); // 2 * 2 = 4; 4 + 2 = 6
multiply2(add2(2)); // 2 + 2 = 4; 4 * 2 = 8

Jak pokazuje poniższy przykład operacje wykonywane są w kolejności „od wewnątrz” w kierunku „zewnętrznym”.

Jedną z „niedogodności” takiego zapisu jest to, że w przypadku większej liczby operacji, możemy lekko się w tym pogubić. Mając np. 10 operacji wyglądałoby to mniej więcej tak…

add2(multiply2(add2(multiply2(add2(multiply2(multiply2(add2(multiply2(add2(2)))))))))) // 174

Jest to niezbyt czytelne… Tutaj niektórym może przypomnieć się kodowanie w React i HOC, withRouter() i kilka innych „with’ów” itd. 😉

Rozwiązaniem jest omawiany w artykule temat i napisanie własnej funkcji do obsługi kompozycji. W internecie można znaleźć kilka pomysłów. Wykonywanie funkcji od lewej do prawej, bądź od prawej do lewej (co pokażę za chwilę).
Istnieją też bardziej złożone rozwiązania, które pozwalają na zdefiniowanie dodatkowych warunków, dla których funkcja ma się wykonać.

Jak mogłaby wyglądać funkcja do obsługi kompozycji?

const compose = (...functions) => args => functions.reduceRight((arg, fn) => fn(arg), args);

Funkcja compose() to doskonały przykład higher order function. Funkcja compose() jako argumenty przyjmuje funkcje, zwracając inną funkcję jako rezultat. Dzięki zastosowaniu spread operatora (…functions), możemy przyjąć zmienną liczbę argumentów, możemy podać jedną, dwie lub wiele funkcji… Następnie dzięki zastosowaniu funkcji reduceRight() lub funkcji reduce() funkcje wykonują się w zdefiniowanej kolejności (od lewej do prawej) a rezultat jednej funkcji jest przekazywany do następnej.

Teraz możemy wywołać funkcję w następujący sposób i sprawdzić działanie:

add2(multiply2(2)); // 2 * 2 = 4, 4 + 2 = 6

compose(
  add2,
  multiply2
)(2); // 2 * 2 = 4; 4 + 2 = 6

W tym przypadku funkcje również wykonywane są od wewnątrz z racji użycia reduceRight()

Możemy zmodyfikować funkcję compose(), zmieniając reduceRight() na reduce(), wtedy funkcje będą wykonywane w kolejności przekazanych parametrów w funkcji compose(). Przy takim podejściu, możemy spotkać pod nazwą pipe() zamiast compose()

const pipe = (...functions) => args => functions.reduce((arg, fn) => fn(arg), args);

pipe(
  add2,
  multiply2
)(2); // 2 +2 = 4; 4 * 2 = 8

Co jeszcze daje nam zastosowanie takiego podejścia?
Przede wszystkim jesteśmy w stanie wielokrotnie używać danego fragmentu logiki. Dla naszego prostego przykładu możemy dodawać, czy mnożyć wynik w różnych innych algorytmach korzystając z tej samej funkcji, bez powielania kodu.
Tak jak wspomniałem, warto pamiętać o tym, aby funkcje były czyste. Oczywiście może zdarzyć się sytuacja, że będziemy potrzebować napisać jakiś losowy generator danych i tutaj też możemy zrobić to kompozycją… (np. generuj losowy ciąg znaków, zakoduj ciąg znaków itd., gdzie każda operacja będzie rozbita na mniejsze elementy)
Dzięki kompozycji kod staje się też czytelniejszy. Łatwiejsza jest również rozbudowa kodu.
W artykule mamy bardzo prosty przykład, więc w przypadku bardziej złożonych algorytmów trzeba przemyśleć, czy takie rozwiązanie będzie odpowiednie.

Na zakończenie chciałbym polecić inne artykuły, które opublikowałem wcześniej, a które przydadzą się jeżeli chodzi o elementy programowania funkcyjnego oraz kompozycji:

Dziękuję za dotrwanie do końca artykułu. Teraz mam nadzieję, że wiesz dlaczego artykuł został opatrzony takim zdjęciem, na którym mamy klocki lego… 🙂 Kompozycję możemy w pewien sposób porównać do klocków, z których budujemy dany algorytm, przy czym położenie jednego klocka wpływa na inne klocki oraz na całą budowlę.

Niebawem kolejne artykuły 🙂

Źródła:
Obraz: https://unsplash.com/photos/US9Tc9pKNBU

Łukasz
Autor

Full-Stack Developer (React/PHP/MySQL). Przygodę z technologiami web rozpocząłem jeszcze jako nastolatek-pasjonat. Przez lata zdobywałem doświadczenie jako freelancer. Od ponad 7 lat pracuję dla dużych firm polskich i międzynarodowych, uczestnicząc w kompleksowych projektach IT. Liczę, że wiedza i przemyślenia, którymi dzielę się na tym blogu, będą Ci pomocne.

Napisz komentarz

Zwracam szczególną uwagę na kulturę komentarzy i nie będę akceptował żadnego hejtu. Jeżeli masz ochotę coś skrytykować, proszę bardzo - konstruktywna krytyka jest wskazana.

− 5 = 5