Darmowy fragment publikacji:
Tytuł oryginału: Functional Programming in JavaScript: How to improve your JavaScript programs using
functional techniques
Tłumaczenie: Tomasz Walczak
ISBN: 978-83-283-3252-2
Original edition copyright © 2016 by Manning Publications Co.
All rights reserved
Polish edition copyright © 2017 by HELION SA.
All rights reserved.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means,
electronic or mechanical, including photocopying, recording or by any information storage retrieval system,
without permission from the Publisher.
Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej
publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną,
fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje
naruszenie praw autorskich niniejszej publikacji.
Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich
właścicieli.
Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były
kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane
z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie
ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji
zawartych w książce.
Wydawnictwo HELION
ul. Kościuszki 1c, 44-100 GLIWICE
tel. 32 231 22 19, 32 230 98 63
e-mail: helion@helion.pl
WWW: http://helion.pl (księgarnia internetowa, katalog książek)
Pliki z przykładami omawianymi w książce można znaleźć pod adresem:
ftp://ftp.helion.pl/przyklady/prfujs.zip
Drogi Czytelniku!
Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres
http://helion.pl/user/opinie/prfujs
Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.
Printed in Poland.
• Kup książkę
• Poleć książkę
• Oceń książkę
• Księgarnia internetowa
• Lubię to! » Nasza społeczność
Przedmowa
Podziękowania
O książce
O autorze
Spis treści
9
11
13
17
1.3. Zalety programowania funkcyjnego
1.1. Czy programowanie funkcyjne może być pomocne?
1.2. Czym jest programowanie funkcyjne?
1.3.1. Ułatwianie podziału złożonych zadań
1.3.2. Przetwarzanie danych za pomocą płynnych łańcuchów wywołań
1.3.3. Radzenie sobie ze złożonością aplikacji asynchronicznych
1.2.1. Programowanie funkcyjne jest deklaratywne
1.2.2. Czyste funkcje i problemy z efektami ubocznymi
1.2.3. Przejrzystość referencyjna i możliwość podstawiania
1.2.4. Zachowywanie niemodyfikowalności danych
CZĘŚĆ I. MYŚL FUNKCYJNIE........................................................................ 19
Rozdział 1. Przechodzenie na model funkcyjny
21
23
24
26
27
31
33
34
34
36
38
40
41
42
42
49
49
52
54
56
56
57
59
61
62
64
65
66
67
70
2.2.1. Zarządzanie stanem obiektów w JavaScripcie
2.2.2. Traktowanie obiektów jak wartości
2.2.3. Głębokie zamrażanie potencjalnie zmiennych elementów
2.2.4. Poruszanie się po grafach obiektów i ich modyfikowanie za pomocą soczewek
Rozdział 2. Operacje wyższego poziomu w JavaScripcie
2.1. Dlaczego JavaScript?
2.2. Programowanie funkcyjne a programowanie obiektowe
1.4. Podsumowanie
2.3. Funkcje
2.3.1. Funkcje jako pełnoprawne obiekty
2.3.2. Funkcje wyższego poziomu
2.3.3. Sposoby uruchamiania funkcji
2.3.4. Metody używane dla funkcji
2.4. Domknięcia i zasięg
2.4.1. Problemy z zasięgiem globalnym
2.4.2. Zasięg funkcji w JavaScripcie
2.4.3. Zasięg pseudobloku
2.4.4. Praktyczne zastosowania domknięć
2.5. Podsumowanie
Poleć książkęKup książkę6
Spis treści
3.1. Przepływ sterowania w aplikacji
3.2. Łączenie metod w łańcuch
3.3. Łączenie funkcji w łańcuch
4.6. Zarządzanie przepływem sterowania z użyciem kombinatorów funkcji
3.6. Podsumowanie
Rozdział 4. W kierunku modularnego kodu do wielokrotnego użytku
3.4. Analizowanie kodu
3.5. Naucz się myśleć rekurencyjnie
3.5.1. Czym jest rekurencja?
3.5.2.
3.5.3. Rekurencyjnie definiowane struktury danych
Jak nauczyć się myśleć rekurencyjnie?
3.3.1. Wyrażenia lambda
3.3.2. Przekształcanie danych za pomocą operacji _.map
3.3.3. Pobieranie wyników za pomocą operacji _.reduce
3.3.4. Usuwanie niepotrzebnych elementów za pomocą funkcji _.filter
CZĘŚĆ II. WKROCZ W ŚWIAT PROGRAMOWANIA FUNKCYJNEGO................71
73
Rozdział 3. Niewielka liczba struktur danych i wiele operacji
74
75
76
77
78
80
84
85
3.4.1. Deklaratywne łańcuchy funkcji w podejściu leniwym
86
3.4.2. Dane w formacie podobnym do SQL-owego — traktowanie funkcji jak danych 90
91
92
92
95
98
99
100
101
102
103
103
104
107
110
111
113
115
115
116
117
118
121
123
124
126
126
126
127
128
128
130
4.5.1. Kompozycja na przykładzie kontrolek HTML-owych
4.5.2. Kompozycja funkcyjna — oddzielenie opisu od przetwarzania
4.5.3. Kompozycja z użyciem bibliotek funkcyjnych
4.5.4. Radzenie sobie z kodem czystym i nieczystym
4.5.5. Wprowadzenie do programowania bezargumentowego
4.3.1. Emulowanie fabryk funkcji
4.3.2. Tworzenie przeznaczonych do wielokrotnego użytku szablonów funkcji
4.1. Łańcuchy metod a potoki funkcji
4.1.1. Łączenie metod w łańcuchy
4.1.2. Porządkowanie funkcji w potoku
4.2. Wymogi dotyczące zgodności funkcji
4.4. Częściowe wywoływanie funkcji i wiązanie parametrów
4.4.1. Rozszerzanie podstawowego języka
4.4.2. Wiązanie funkcji wykonywanych z opóźnieniem
4.5. Tworzenie potoków funkcji za pomocą kompozycji
4.2.1. Funkcje zgodne ze względu na typ
4.2.2. Funkcje i arność — argument na rzecz stosowania krotek
4.3. Przetwarzanie funkcji z rozwijaniem
4.6.1. Kombinator identity
4.6.2. Kombinator tap
4.6.3. Kombinator alt
4.6.4. Kombinator seq
4.6.5. Kombinator fork
4.7. Podsumowanie
Poleć książkęKup książkęSpis treści
Rozdział 5. Wzorce projektowe pomagające radzić sobie ze złożonością
5.1. Wady imperatywnej obsługi błędów
5.1.1. Obsługa błędów za pomocą bloków try-catch
5.1.2. Dlaczego w programach funkcyjnych nie należy zgłaszać wyjątków?
5.1.3. Problemy ze sprawdzaniem wartości null
5.2. Budowanie lepszego rozwiązania — funktory
5.2.1. Opakowywanie niebezpiecznych wartości
5.2.2. Funktory
5.3. Funkcyjna obsługa błędów z użyciem monad
5.3.1. Monady — od przepływu sterowania do przepływu danych
5.3.2. Obsługa błędów za pomocą monad Maybe i Either
5.3.3.
Interakcje z zewnętrznymi zasobami przy użyciu monady IO
5.4. Monadyczne łańcuchy i kompozycje
5.5. Podsumowanie
7
131
132
132
133
134
135
136
138
140
141
145
154
157
163
CZĘŚĆ III. ROZWIJANIE UMIEJĘTNOŚCI W ZAKRESIE
Rozdział 6. Zabezpieczanie kodu przed błędami
6.1. Wpływ programowania funkcyjnego na testy jednostkowe
6.2. Problemy z testowaniem programów imperatywnych
6.3. Testowanie kodu funkcyjnego
6.3.1. Traktowanie funkcji jak czarnych skrzynek
6.3.2. Koncentracja na logice biznesowej zamiast na przepływie sterowania
6.3.3. Oddzielanie czystego kodu od nieczystego za pomocą monadycznej izolacji
6.3.4. Tworzenie atrap zewnętrznych zależności
PROGRAMOWANIA FUNKCYJNEGO .......................................... 165
167
168
169
6.2.1. Trudność identyfikowania i wyodrębniania zadań
170
6.2.2. Zależność od współużytkowanych zasobów prowadzi do niespójnych wyników 171
172
6.2.3. Zdefiniowana kolejność wykonywania operacji
173
173
174
176
178
180
186
187
190
193
195
196
198
200
202
203
204
206
207
207
7.3.1. Memoizacja
7.3.2. Memoizacja funkcji o dużych wymaganiach obliczeniowych
7.2.1. Unikanie obliczeń dzięki kombinatorowi funkcyjnemu alt
7.2.2. Wykorzystanie syntezy wywołań
7.1.1. Rozwijanie funkcji a kontekst funkcji na stosie
7.1.2. Wyzwania związane z kodem rekurencyjnym
6.4. Przedstawianie specyfikacji w testach opartych na cechach
6.5. Pomiar efektywności testów na podstawie pokrycia kodu
6.5.1. Pomiar efektywności testów kodu funkcyjnego
6.5.2. Pomiar złożoności kodu funkcyjnego
7.2. Odraczanie wykonywania funkcji za pomocą leniwego wartościowania
7.3. Wywoływanie kodu wtedy, gdy jest potrzebny
6.6. Podsumowanie
Rozdział 7. Optymalizacje funkcyjne
7.1. Praca funkcji na zapleczu
Poleć książkęKup książkę8
Spis treści
7.3.3. Wykorzystanie rozwijania funkcji i memoizacji
7.3.4. Dekompozycja w celu zastosowania memoizacji do maksymalnej
liczby komponentów
7.3.5. Stosowanie memoizacji do wywołań rekurencyjnych
7.4. Rekurencja i optymalizacja wywołań ogonowych
7.4.1. Przekształcanie wywołań nieogonowych w ogonowe
7.5. Podsumowanie
Rozdział 8. Zarządzanie asynchronicznymi zdarzeniami i danymi
8.1. Problemy związane z kodem asynchronicznym
8.1.1. Tworzenie związanych z czasem zależności między funkcjami
8.1.2. Powstawanie piramidy wywołań zwrotnych
8.1.3. Styl oparty na przekazywaniu kontynuacji
8.2. Pełnoprawne operacje asynchroniczne oparte na obietnicach
8.2.1. Łańcuchy metod wykonywanych w przyszłości
8.2.2. Kompozycja operacji synchronicznych i asynchronicznych
8.3. Leniwe generowanie danych
8.3.1. Generatory i rekurencja
8.3.2. Protokół iteratorów
8.4. Programowanie funkcyjne i reaktywne z użyciem biblioteki RxJS
8.4.1. Dane jako obserwowalne sekwencje
8.4.2. Programowanie funkcyjne i reaktywne
8.4.3. RxJS i obietnice
8.5. Podsumowanie
Dodatek. Biblioteki JavaScriptu używane w książce
Skorowidz
210
211
212
213
215
218
219
220
221
222
224
227
230
235
237
239
241
242
242
243
246
246
249
253
Poleć książkęKup książkęOperacje wyższego poziomu
w JavaScripcie
Zawartość rozdziału:
Dlaczego JavaScript może być językiem funkcyjnym?
JavaScript jako język umożliwiający programowanie
w wielu paradygmatach
Niemodyfikowalność i polityka modyfikacji
Funkcje wyższego poziomu i funkcje pełnoprawne
Domknięcia i zasięg
Praktyczne zastosowanie domknięć
W językach naturalnych nie występuje dominujący paradygmat. Podobnie jest
z JavaScriptem. Programiści mogą wybierać spośród wielu podejść — proceduralnego,
funkcyjnego i obiektowego — oraz łączyć je w odpowiedni sposób.
— Angus Croll, If Hemingway Wrote JavaScript
Wraz z rozrastaniem się aplikacji rośnie też ich złożoność. Niezależnie od tego, jak
wysoko oceniasz swoje umiejętności, bez odpowiednich modeli programowania nie da
się uniknąć chaosu. W rozdziale 1. wyjaśniłem powody, dla których programowanie
funkcyjne jest atrakcyjnym paradygmatem. Jednak same paradygmaty to tylko modele,
które trzeba ożywić za pomocą właściwego języka.
W tym rozdziale zaprezentuję Ci krótki przegląd hybrydowego języka łączącego
cechy obiektowe i funkcyjne. Jest to JavaScript. Oczywiście nie będzie to kompletne
omówienie języka. Zamiast tego skoncentruję się na aspektach, które umożliwiają
Poleć książkęKup książkę42
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
funkcyjne używanie JavaScriptu, a także wyjaśnię jego wady. Jedną z nich jest brak
zapewniania niemodyfikowalności. W tym rozdziale omawiam też funkcje wyższego
poziomu i domknięcia (ang. closure). Te techniki to podstawa umożliwiająca pisanie
kodu w JavaScripcie w funkcyjny sposób. To tyle tytułem wstępu — pora zaczynać.
2.1. Dlaczego JavaScript?
Zacząłem od wyjaśnienia, dlaczego warto stosować podejście funkcyjne. Inne pytanie,
które można sobie zadać, brzmi: „Dlaczego JavaScript?”. Odpowiedź jest prosta: „Ponie-
waż jest wszechobecny”. JavaScript to obiektowy język ogólnego przeznaczenia z dyna-
micznie określanymi typami i bardzo ekspresywną składnią. Jest to jeden z najbardziej
rozpowszechnionych języków programowania w historii. Możesz zetknąć się z nim
w kontekście rozwijania aplikacji mobilnych, witryn, serwerów WWW, aplikacji na
komputery stacjonarne, aplikacji wbudowanych, a nawet baz danych. Z powodu nie-
zwykłej popularności JavaScriptu jako języka internetu można bezpiecznie założyć, że
jest to zdecydowanie najczęściej stosowany język funkcyjny.
Choć składnia JavaScriptu przypomina składnię C, twórcy JavaScriptu w dużym
stopniu inspirowali się językami funkcyjnymi takimi jak Lisp i Scheme. Podobieństwa
polegają na obsłudze funkcji wyższego poziomu, domknięć, literałów tablicowych
i innych mechanizmów, dzięki którym JavaScript to świetna technologia do stosowa-
nia technik funkcyjnych. Funkcje w JavaScripcie są główną jednostką pracy. Oznacza to,
że funkcje nie tylko sterują operacjami aplikacji, ale też służą do definiowania obiektów,
tworzenia modułów i obsługi zdarzeń.
JavaScript wciąż jest modyfikowany i usprawniany. Opis języka znajdziesz w stan-
dardzie ECMAScript (ES). W najnowszej jego wersji, ES6, pojawiło się wiele dodat-
kowych mechanizmów: funkcje ze strzałką, stałe, iteratory, obietnice (ang. promises)
i inne konstrukty dobrze dostosowane do programowania funkcyjnego.
Choć JavaScript obejmuje wiele przydatnych mechanizmów funkcyjnych, warto
zauważyć, że jest to język w równym stopniu obiektowy i funkcyjny. Niestety, aspekt
funkcyjny często jest pomijany. Większość programistów stosuje operacje modyfikujące
dane i imperatywne struktury kontrolne oraz zmienia stan obiektów. W podejściu funk-
cyjnym wszystko to trzeba wyeliminować. Mimo to uważam, że warto najpierw poświę-
cić trochę czasu na omówienie JavaScriptu jako języka obiektowego. Dzięki temu lepiej
docenisz różnice między oboma paradygmatami i łatwiej będzie Ci zmienić podejście
na funkcyjne.
2.2. Programowanie funkcyjne
a programowanie obiektowe
Zarówno model funkcyjny, jak i model obiektowy umożliwiają rozwijanie średnich
i dużych systemów. W językach hybrydowych (takich jak Scala i F#) oba te paradyg-
maty są łączone. JavaScript też to umożliwia. Aby dobrze opanować JavaScript, należy
nauczyć się łączyć oba podejścia. Decyzję o ich proporcjach należy podejmować na
podstawie osobistych preferencji i wymagań rozwiązywanego problemu. Zrozumienie,
Poleć książkęKup książkę2.2. Programowanie funkcyjne a programowanie obiektowe
43
w jakich miejscach podejścia funkcyjne i obiektowe się przenikają i różnią, pomoże Ci
przechodzić od jednego do drugiego i myśleć w kategoriach dowolnego z tych modeli.
Zastanów się nad prostym modelem systemu do zarządzania uczelnią, obejmującego
typ Student. W kontekście hierarchii klas lub typów naturalne jest przyjęcie, że Student
to podtyp typu Person obejmującego podstawowe atrybuty takie jak imię, nazwisko,
adres itd.
Obiektowy JavaScript
Gdy definiuję relację między obiektami i piszę, że jeden z nich jest podtypem (lub typem
pochodnym) drugiego, mam na myśli relację między prototypami obiektów. Należy
zauważyć, że choć JavaScript jest językiem obiektowym, nie występuje tu klasyczne
dziedziczenie znane z innych języków (na przykład z Javy).
W wersji ES6 mechanizm do tworzenia relacji między prototypami obiektów został wzbo-
gacony o „lukier składniowy” w postaci słów kluczowych class i extends (co zdaniem wielu
osób było błędną decyzją). Nowa składnia pozwala jednoznacznie zapisać dziedziczenie
obiektów, jednak ukrywa działanie i możliwości mechanizmu prototypów z JavaScriptu.
W tej książce nie omawiam jednak obiektowego JavaScriptu (w końcowej części rozdziału
polecam książkę ze szczegółowym omówieniem tego zagadnienia i innych tematów).
Nowe możliwości można dodać, tworząc bardziej specyficzny typ pochodny od Student,
na przykład CollegeStudent. W programach obiektowych tworzenie nowych typów
pochodnych jest podstawowym narzędziem umożliwiającym wielokrotne wykorzy-
stanie kodu. Tu w typie CollegeStudent można wykorzystać wszystkie dane i operacje
z typów nadrzędnych. Jednak dodawanie nowych mechanizmów do istniejących typów
bywa kłopotliwe, jeśli dany mechanizm nie dotyczy wszystkich typów pochodnych.
Choć pola firstname i lastname dotyczą typu Person i wszystkich typów pochodnych,
pole workAddress powinno znaleźć się w typie Employee (pochodnym od Person), ale
już nie w typie Student. Ten model omawiam dlatego, że podstawową różnicą między
aplikacjami obiektowymi i funkcyjnymi jest sposób uporządkowania danych (właści-
wości obiektów) i operacji (funkcji).
W aplikacjach obiektowych (są one przede wszystkim imperatywne) często sto-
suje się opartą na obiektach hermetyzację, aby chronić integralność modyfikowalnego
stanu — odziedziczonego lub utworzonego bezpośrednio. Aby modyfikować i pobie-
rać stan, należy posługiwać się metodami instancji. Występuje tu więc ścisłe powiązanie
między danymi obiektu a jego szczegółowymi operacjami. W ten sposób powstaje
nierozłączna całość. Jest to cel programowania obiektowego i powód, dla którego
główną formą abstrakcji w tym podejściu są obiekty.
W programowaniu funkcyjnym nie trzeba ukrywać danych przed jednostkami wywo-
łującymi. W tym podejściu zwykle używany jest mniejszy zbiór bardzo prostych typów
danych. Ponieważ wszystkie elementy są niemodyfikowalne, można bezpośrednio korzy-
stać z obiektów za pomocą ogólnych funkcji znajdujących się poza zasięgiem obiektu.
Oznacza to luźne powiązanie danych z operacjami. Na rysunku 2.1 widać, że w podej-
ściu funkcyjnym zamiast szczegółowych metod instancji używane są ogólniejsze operacje,
które mogą działać dla wielu typów danych. W tym paradygmacie to funkcje stają się
podstawową formą abstrakcji.
Poleć książkęKup książkę44
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
Rysunek 2.1. Programowanie
obiektowe zachęca do logicznego
łączenia wielu typów danych
z wyspecjalizowanymi
operacjami, natomiast
w programowaniu funkcyjnym
operacje są łączone z typami
danych za pomocą kompozycji.
Istnieje optymalny punkt,
w którym można produktywnie
stosować oba paradygmaty.
W językach hybrydowych,
takich jak Scala, F# i JavaScript,
można posługiwać się oboma
podejściami
Na rysunku 2.1 widać, że oba paradygmaty różnią się w górnej i prawej części wykresu.
W praktyce najlepsze fragmenty kodu obiektowego, jakie widziałem, były napisane
z użyciem obu paradygmatów (co odpowiada przecięciu linii na wykresie). W tym
modelu obiekty należy traktować jak niemodyfikowalne encje (wartości) i zapisywać
związane z nimi operacje w postaci funkcji działających na tych obiektach. Przyjrzyj
się następującej metodzie z typu Person:
get fullname() {
return [this._firstname, this._lastname].join( );
}
Można ją przenieść poza typ w następujący sposób:
var fullname =
person = [person.firstname, person.lastname].join( );
Wiesz już, że JavaScript to język z dynamicznie określanymi typami. To oznacza, że
nie musisz jawnie podawać typu obok nazw obiektów. Dlatego wywołanie fullname()
działa dla dowolnego typu pochodnego od Person (i dowolnego obiektu z właściwo-
ściami firstname oraz lastname), co ilustruje rysunek 2.2. Z powodu dynamicznej natury
JavaScript umożliwia stosowanie uogólnionych funkcji polimorficznych. To oznacza,
że funkcje używające referencji do typów bazowych (na przykład typu Person) dzia-
łają także dla obiektów typów pochodnych (takich jak Student lub CollegeStudent).
Obiekt „this”
jest zastępowany
przekazanym obiektem.
W metodach łatwo jest używać
słowa „this”, aby uzyskać
dostęp do stanu danego obiektu.
Na rysunku 2.2 widać, że przekształcenie fullname() w niezależną funkcję pozwala
zrezygnować z używania słowa kluczowego this przy dostępie do danych obiektów.
Używanie słowa this to problematyczne rozwiązanie, ponieważ daje dostęp do danych
z poziomu instancji poza zasięgiem metody, co oznacza efekty uboczne. W programo-
waniu funkcyjnym dane obiektu nie są ściśle powiązane z konkretnymi fragmentami
rozwiązania, co ułatwia wielokrotne wykorzystanie kodu i jego konserwację.
Zamiast tworzyć wiele typów pochodnych, możesz rozbudować działanie funkcji,
przekazując jako argumenty inne funkcje. Aby to zilustrować, na listingu 2.1 definiuję
prosty model danych. Listing ten obejmuje klasę Student pochodną od Person. Model ten
jest używany w większości przykładów z tej książki.
Poleć książkęKup książkę2.2. Programowanie funkcyjne a programowanie obiektowe
45
Rysunek 2.2. W programowaniu obiektowym istotne jest tworzenie hierarchii dziedziczenia
(na przykład typu Student po typie Parent), w której metody i dane są ze sobą ściśle powiązane.
W programowaniu funkcyjnym preferowane są ogólne funkcje polimorficzne działające dla różnych
typów danych. W tym podejściu programiści unikają stosowania słowa this
Listing 2.1. Definicje klas Person i Student
class Person {
constructor(firstname, lastname, ssn) {
this._firstname = firstname;
this._lastname = lastname;
this._ssn = ssn;
this._address = null;
this._birthYear = null;
}
get ssn() {
return this._ssn;
}
get firstname() {
return this._firstname;
}
get lastname() {
return this._lastname;
}
get address() {
return this._address;
}
Poleć książkęKup książkę46
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
get birthYear() {
return this._birthYear;
}
set birthYear(year) {
this._birthYear = year;
}
set address(addr){
this._address = addr;
}
Settery nie mają umożliwiać modyfikowania
obiektów. Mają natomiast pozwalać łatwo
tworzyć obiekty o różnych właściwościach
bez używania długich konstruktorów.
Po utworzeniu obiektów i określeniu ich
danych stan nigdy się nie zmienia. W dalszej
części rozdziału zobaczysz, jak to zapewnić.
toString() {
return `Person(${this._firstname}, ${this._lastname})`;
}
}
class Student extends Person {
constructor(firstname, lastname, ssn, school) {
super(firstname, lastname, ssn);
this._school = school;
}
get school() {
return this._school;
}
}
Pobieranie i uruchamianie przykładowego kodu
Oryginalny przykładowy kod z tej książki znajdziesz na stronach http://www.manning.com/
books/functionalprogramming-in-javascript i https://github.com/luijar/functional-program
ming-js. Spolszczona wersja jest dostępna pod adresem ftp://ftp.helion.pl/przyklady/
prfujs.zip. Możesz swobodnie wypróbować projekty i samodzielnie przećwiczyć progra-
mowanie funkcyjne. Zachęcam do uruchamiania dowolnych testów jednostkowych i eks-
perymentowania z różnymi programami. W czasie, gdy powstaje ta książka, nie wszystkie
mechanizmy z wersji ES6 JavaScriptu są zaimplementowane w każdej przeglądarce.
Dlatego używam transpilatora Babel (jego wcześniejsza nazwa to 6to5) do przekształ-
cania kodu z wersji ES6 na ES5.
Niektóre mechanizmy nie wymagają transpilacji i można je stosować po włączeniu odpo-
wiedniego ustawienia w przeglądarce (na przykład Eksperymentalny JavaScript w prze-
glądarce Chrome). Jeśli uruchamiasz przeglądarkę w trybie eksperymentalnym, ważne
jest, aby włączyć tryb strict. W tym celu dodaj instrukcję use strict ; na początku pliku
z kodem w JavaScripcie.
Zadanie polega na tym, aby po pobraniu danych osoby znaleźć wszystkich jej znajo-
mych mieszkających w tym samym kraju. Inne zadanie to wyszukiwanie na podstawie
danych studenta innych studentów z tego samego państwa i tej samej uczelni. W roz-
wiązaniu obiektowym operacje są ściśle powiązane (za pomocą słów this i super) z typami
bazowym i pochodnym.
// W klasie Person
peopleInSameCountry(friends) {
var result = [];
for (let idx in friends) {
Poleć książkęKup książkę2.2. Programowanie funkcyjne a programowanie obiektowe
47
var friend = friends [idx];
if (this.address.country === friend.address.country) {
result.push(friend);
}
}
return result;
};
Słowo „super” służy
do żądania danych
od klasy bazowej.
// W klasie Student
studentsInSameCountryAndSchool(friends) {
var closeFriends = super.peopleInSameCountry(friends);
var result = [];
for (let idx in closeFriends) {
var friend = closeFriends[idx];
if (friend.school === this.school) {
result.push(friend);
}
}
return result;
};
Natomiast w programowaniu funkcyjnym ważne są czystość funkcji i przejrzystość
referencyjna, dlatego dzięki odizolowaniu działań od stanu można dodawać nowe ope-
racje, definiując i tworząc za pomocą kompozycji nowe funkcje operujące na określo-
nych typach. W ten sposób powstają proste obiekty odpowiedzialne za przechowy-
wanie danych i wszechstronne funkcje, które mogą operować na tych obiektach
przekazywanych jako argumenty. Funkcje te można łączyć za pomocą kompozycji,
aby uzyskać wyspecjalizowane możliwości. Na razie nie uczyłeś się jeszcze kompozycji
(omawiam ją w rozdziale 4.), jest ona jednak istotna, aby można było pokazać następną
ważną różnicę między opisywanymi paradygmatami. To, co dziedziczenie zapewnia
w kontekście obiektowym, kompozycja umożliwia w programowaniu funkcyjnym,
ponieważ pozwala stosować nowe operacje do różnych typów danych1. Do wykonywania
kodu posłuży następujący zbiór danych:
var curry = new Student( Haskell , Curry ,
111-11-1111 , Penn State );
curry.address = new Address( USA );
var turing = new Student( Alan , Turing ,
222-22-2222 , Princeton );
turing.address = new Address( Anglia );
var church = new Student( Alonzo , Church ,
333-33-3333 , Princeton );
church.address = new Address( USA );
var kleene = new Student( Stephen , Kleene ,
444-44-4444 , Princeton );
kleene.address = new Address( USA );
1 Dotyczy to w większym stopniu użytkowników modelu obiektowego niż samego paradygmatu.
Wielu ekspertów, w tym „banda czterech”, zaleca stosowanie kompozycji obiektów zamiast dzie-
dziczenia klas (zgodnie z zasadą podstawiania Liskov).
Poleć książkęKup książkę48
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
W podejściu obiektowym do wyszukiwania wszystkich innych studentów uczęszcza-
jących na tę samą uczelnię służy metoda z typu Student:
church.studentsInSameCountryAndSchool([curry, turing, kleene]);
//- [kleene]
W rozwiązaniu funkcyjnym problem jest rozbity na mniejsze funkcje:
function selector(country, school) {
return function(student) {
return student.address.country() === country
student.school() === school;
};
}
Poruszanie się po grafach
obiektów. Dalej w rozdziale
pokazuję lepszy sposób dostępu
do atrybutów obiektów.
Funkcja selector potrafi porównywać państwa
i uczelnie powiązane ze studentami.
var findStudentsBy = function(friends, selector) {
return friends.filter(selector);
};
Użycie operacji filter do tablicy
i dodanie specjalnej operacji
za pomocą funkcji selector.
findStudentsBy([curry, turing, church, kleene],
selector( USA , Princeton ));
//- [church, kleene]
Dzięki zastosowaniu programowania funkcyjnego możesz utworzyć zupełnie nową
funkcję, findStudentsBy, która jest znacznie łatwiejsza w użyciu niż kod obiektowy.
Pamiętaj, że nowa funkcja działa dla dowolnych obiektów powiązanych z typem Person,
a także dla dowolnej kombinacji uczelni i państwa.
Ten przykład dobrze ilustruje różnice między omawianymi paradygmatami. W pro-
jektowaniu obiektowym nacisk położony jest na naturę danych i relacje między nimi,
natomiast w programowaniu funkcyjnym ważne są wykonywane operacje, czyli dzia-
łanie kodu. Tabela 2.1 zawiera podsumowanie najważniejszych różnic, na które warto
zwrócić uwagę. Omawiam je w tym rozdziale i w dalszych fragmentach książki.
Tabela 2.1. Porównanie wybranych ważnych cech programowania obiektowego i funkcyjnego.
Cechy te omawiam w książce
Programowanie funkcyjne Programowanie obiektowe
Jednostka kompozycji
Styl programowania
Dane i operacje
Zarządzanie stanem
Funkcje
Deklaratywny
Luźno powiązane za pomocą
czystych i niezależnych funkcji
Obiekty są traktowane jak
niemodyfikowalne wartości
Sterowanie przepływem Funkcje i rekurencja
Bezpieczeństwo wątków Umożliwia programowanie
Obiekty (klasy)
Imperatywny
Ściśle powiązane za pomocą klas
z metodami
Głównie modyfikowanie obiektów
za pomocą metod instancji
Pętle i instrukcje warunkowe
Trudne do osiągnięcia
Hermetyzacja
współbieżne
Niepotrzebna, ponieważ dane
są niemodyfikowalne
Potrzebna do ochrony integralności
danych
Mimo różnic między tymi paradygmatami tworzenie aplikacji z użyciem ich obu może
okazać się bardzo przydatnym podejściem. Z jednej strony uzyskujesz rozbudowany
Poleć książkęKup książkę2.2. Programowanie funkcyjne a programowanie obiektowe
49
model dziedziny z naturalnymi relacjami między tworzącymi go typami. Z drugiej
strony otrzymujesz zestaw czystych funkcji operujących na tych typach. To, z którego
modelu będziesz korzystał w większym stopniu, zależy od tego, na ile komfortowo czu-
jesz się, posługując się oboma paradygmatami. Ponieważ JavaScript jest w równym
stopniu obiektowy i funkcyjny, używanie go w sposób funkcyjny wymaga poświęcenia
specjalnej uwagi kontrolowaniu zmian stanu.
2.2.1. Zarządzanie stanem obiektów w JavaScripcie
Stan programu można zdefiniować jako zapis danych przechowywanych w określonym
momencie we wszystkich obiektach. Niestety, JavaScript jest jednym z najgorszych
języków, jeśli chodzi o zabezpieczanie stanu obiektów. Obiekty w JavaScripcie są
wysoce dynamiczne. Można w dowolnym momencie modyfikować, dodawać i usuwać
właściwości obiektów. Założenie, że na listingu 2.1 właściwość _address w typie Person
jest hermetyczna, jest błędne (podkreślenie w nazwie to zabieg wyłącznie składniowy).
Także poza klasą Person można uzyskać pełny dostęp do tej właściwości i wykonywać
na niej dowolne operacje (a nawet ją usunąć).
Taka swoboda pociąga za sobą dużą odpowiedzialność. Choć opisany model pozwala
wykonywać wiele wygodnych operacji, na przykład dynamicznie tworzyć właściwości,
może prowadzić do powstawania bardzo trudnego w konserwacji kodu, gdy program
jest średni lub duży.
W rozdziale 1. wspomniałem, że praca z czystymi funkcjami ułatwia konserwację
i analizowanie kodu. Czy istnieje coś takiego jak „czysty obiekt”? Niemodyfikowalny
obiekt z niemodyfikującymi danych operacjami można uznać za czysty. To samo wnio-
skowanie, które przedstawiłem dla funkcji, można zastosować także do prostych obiek-
tów. Zarządzanie stanem w JavaScripcie jest niezwykle istotne, jeśli chcesz używać go
jak języka funkcyjnego. Znane są praktyki i wzorce pozwalające zapewnić niemodyfi-
kowalność (poznasz je w dalszych punktach), jednak zapewnienie kompletnej hermety-
zacji i ochrony danych zależy od dyscypliny programisty.
2.2.2. Traktowanie obiektów jak wartości
Łańcuchy znaków i liczby to prawdopodobnie najłatwiejsze typy danych do obsługi
w dowolnym języku programowania. Jak myślisz, dlaczego tak się dzieje? Po części
wynika to z tego, że te proste typy są z natury niemodyfikowalne. Zapewnia to progra-
mistom spokój, którego nie gwarantują typy definiowane przez użytkownika. W pro-
gramowaniu funkcyjnym typy działające w ten sposób są nazywane wartościami
(ang. values). W rozdziale 1. nauczyłeś się zwracać uwagę na niemodyfikowalność.
Wymaga to traktowania każdego obiektu jak wartości. Można wtedy korzystać z funkcji
przekazujących obiekty i nie martwić się o to, że obiekty zostaną zmodyfikowane.
Mimo związanego z klasami „lukru składniowego” dodanego w wersji ES6 obiekty
w JavaScripcie nie są niczym więcej niż zbiorami atrybutów, które można w dowol-
nym momencie dodawać, usuwać i modyfikować. Jak można zapobiec takim opera-
cjom? Wiele języków programowania udostępnia konstrukty zapewniające niemodyfi-
kowalność właściwości obiektu. Jest to na przykład słowo kluczowe final w Javie.
Poleć książkęKup książkę50
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
W językach takich jak F# zmienne są domyślnie niemodyfikowalne, chyba że progra-
mista postanowi inaczej. Obecnie w JavaScripcie brakuje podobnych możliwości. Choć
nie da się zmienić wartości typu prostego, możliwa jest modyfikacja stanu zmiennej
wskazującej taką wartość. Dlatego potrzebna jest możliwość tworzenia, a przynajmniej
symulowania niemodyfikowalnych referencji, co pozwoli korzystać z obiektów typów
definiowanych przez użytkownika w taki sposób, jakby były niemodyfikowalne.
W wersji ES6 do tworzenia stałych referencji służy słowo kluczowe const. Jest to
krok w dobrym kierunku, ponieważ zadeklarowanych tak stałych nie można ponow-
nie zadeklarować. Nie można też zmienić przypisanych do nich wartości. W progra-
mowaniu funkcyjnym w praktyce możesz stosować słowo kluczowe const do dodawania
do programu prostych danych konfiguracyjnych (łańcuchów znaków z adresami URL,
nazw baz danych itd.), gdy są one potrzebne. Choć odczyt danych z zewnętrznej
zmiennej to efekt uboczny, stałe działają w specjalny sposób, dzięki czemu nie zmie-
niają się nieoczekiwanie między wywołaniami funkcji. Oto przykład ilustrujący dekla-
rowanie stałej:
const gravity_ms = 9.806;
gravity_ms = 20;
Jednak to nie zapewnia niemodyfikowalności na poziomie potrzebnym w programo-
waniu funkcyjnym. Możesz zapobiec ponownemu przypisaniu wartości do zmiennej,
jak jednak uniemożliwić zmianę wewnętrznego stanu obiektu? Pokazany poniżej kod
jest w pełni dozwolony:
const student = new Student( Alonzo , Church ,
666-66-6666 , Princeton );
Środowisko uruchomieniowe JavaScriptu
nie pozwoli na to ponowne przypisanie.
Modyfikacja właściwości.
student.lastname = Mourning ;
Potrzebniejsza jest bardziej ścisła polityka zapewniania niemodyfikowalności. Dobrą
strategią ochrony przed modyfikacjami jest hermetyzacja. W prostych strukturach obiek-
towych jedną z możliwości jest zastosowanie wzorca obiekt traktowany jak wartość
(ang. Value Object). Równość obiektów traktowanych jak wartości nie jest oparta na
tożsamości lub referencji, a jedynie na samej wartości. Po zadeklarowaniu takiego
obiektu jego stan nie może się zmieniać. Oprócz liczb i łańcuchów znaków przykła-
dowymi obiektami tego rodzaju są tuple, pair, point, zipCode, coordinate, money, date
itd. Oto kod funkcji zipCode:
function zipCode(code, location) {
let _code = code;
let _location = location || ;
return {
code: function () {
return _code;
},
location: function () {
return _location;
},
fromString: function (str) {
let parts = str.split( - );
Poleć książkęKup książkę2.2. Programowanie funkcyjne a programowanie obiektowe
51
return zipCode(parts[0], parts[1]);
},
toString: function () {
return _code + - + _location;
}
};
}
const princetonZip = zipCode( 08544 , 3345 );
princetonZip.toString(); //- 08544-3345
W JavaScripcie możesz zastosować funkcje i chronić dostęp do wewnętrznego stanu
kodu pocztowego, zwracając interfejs literału obiektowego, który udostępnia jednostce
wywołującej niewielki zbiór metod i traktuje pola _code i _location jako zmienne pseu-
doprywatne. Te zmienne są dostępne w literale obiektowym tylko za pomocą domknięć,
z którymi zapoznasz się w dalszej części rozdziału.
Zwracany obiekt działa jest wartość typu prostego bez metod modyfikujących stan2.
Dlatego choć metoda toString nie jest czystą funkcją, działa jak ona i w czysty sposób
zwraca łańcuch znaków reprezentujący dany obiekt. Obiekty traktowane jak wartości są
„lekkie” i łatwo można z nich korzystać zarówno w modelu funkcyjnym, jak i w podejściu
obiektowym. Za pomocą takich obiektów i słowa kluczowego const można tworzyć
obiekty działające podobnie jak łańcuchy znaków i liczby. Przyjrzyj się następnemu
przykładowi:
function coordinate(lat, long) {
let _lat = lat;
let _long = long;
return {
latitude: function () {
return _lat;
},
longitude: function () {
return _long;
},
translate: function (dx, dy) {
return coordinate(_lat + dx, _long + dy);
},
toString: function () {
return ( + _lat + , + _long + ) ;
}
};
}
Zwraca nowy obiekt
z przekształconymi
współrzędnymi.
const greenwich = coordinate(51.4778, 0.0015);
greenwich.toString(); //- (51.4778, 0.0015)
Używanie metod (takich jak translate) do zwracania nowych obiektów to inny sposób
zapewniania niemodyfikowalności. Przekształcenie danego obiektu skutkuje utworze-
niem nowego obiektu typu coordinate:
2 Wewnętrzny stan obiektu można chronić, jednak działanie obiektu może się zmieniać, ponieważ
dozwolone jest dynamiczne usuwanie lub zastępowanie jego metod.
Poleć książkęKup książkę52
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
greenwich.translate(10, 10).toString(); //- (61.4778, 10.0015)
„Obiekt traktowany jak wartość” to obiektowy wzorzec projektowy zainspirowany
programowaniem funkcyjnym. Jest to kolejny przykład pokazujący, że oba omawiane
paradygmaty w elegancki sposób się uzupełniają. Ten wzorzec ilustruje idealne roz-
wiązanie, jednak w praktyce nie wystarcza do modelowania całych dziedzin proble-
mowych. Kod zwykle musi obsługiwać dane hierarchiczne (takie jak w przykładzie
z typami Person i Student), a także komunikować się z już istniejącymi obiektami. Na
szczęście JavaScript udostępnia funkcję Object.freeze, która pozwala radzić sobie
w takich sytuacjach.
2.2.3. Głębokie zamrażanie potencjalnie zmiennych elementów
W nowej składni klas w JavaScripcie nie występują słowa kluczowe służące do oznacza-
nia pól jako niemodyfikowalnych, jednak obsługiwany jest wewnętrzny mechanizm do
blokowania modyfikacji pól, wykorzystujący ukryte metawłaściwości obiektów, takie jak
writable. Dzięki ustawieniu tej właściwości na false funkcja Object.freeze() Java-
Scriptu zapobiega zmianom stanu obiektu. Zacznijmy od zamrożenia obiektu person
z listingu 2.1:
var person = Object.freeze(new Person( Haskell , Curry , 444-44-4444 ));
person.firstname = Bob ;
Wykonanie tego kodu sprawia, że atrybuty obiektu person stają się przeznaczone tylko
do odczytu. Próba ich modyfikacji (tu dotyczy to pola _firstname) prowadzi do błędu:
TypeError: Cannot assign to read only property _firstname of # Person
Wywołanie Object.freeze() zamraża także odziedziczone atrybuty. Dlatego zamrożenie
obiektu typu Student działa w ten sam sposób i uwzględnia łańcuch prototypów obiektu,
zabezpieczając wszystkie atrybuty odziedziczone po typie Person. Technika ta nie zamraża
jednak atrybutów obiektów zagnieżdżonych (zobacz rysunek 2.3).
Niedozwolone
Oto definicja typu Address:
class Address {
constructor(country, state, city, zip, street) {
this._country = country;
this._state = state;
this._city = city;
this._zip = zip;
this._street = street;
}
get street() {
return this._street;
}
get city() {
return this._city;
}
get state() {
return this._state;
Poleć książkęKup książkę2.2. Programowanie funkcyjne a programowanie obiektowe
53
Rysunek 2.3. Choć typ Person został zamrożony, nie dotyczy to jego wewnętrznych właściwości
obiektowych (na przykład _address). Dlatego w dowolnym momencie można zmodyfikować pole
person.address.country. Ponieważ technika ta dotyczy tylko zmiennych najwyższego poziomu,
jest to zamrażanie płytkie
}
get zip() {
return this._zip;
}
get country() {
return this._country;
}
}
Niestety, pokazany poniżej kod nie powoduje żadnych błędów:
var person = new Person( Haskell , Curry , 444-44-4444 );
person.address = new Address(
USA , NJ , Princeton ,
zipCode( 08544 , 1234 ), Alexander St. );
person = Object.freeze(person);
person.address._country = Francja ; //- dozwolone!
person.address.country; //- Francja
Object.freeze() to płytka operacja. Aby rozwiązać ten problem, trzeba ręcznie zamrozić
zagnieżdżone elementy obiektu, co ilustruje listing 2.2.
Listing 2.2. Funkcja rekurencyjna do głębokiego zamrażania obiektów
var isObject = (val) = val typeof val === object ;
function deepFreeze(obj) {
if(isObject(obj)
Pomija funkcje. Choć technicznie w JavaScripcie funkcje
można modyfikować, tu koncentrujemy się na właściwościach
w postaci danych.
Poleć książkęKup książkę54
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
!Object.isFrozen(obj)) {
Ignoruje obiekty, które już zostały
zamrożone, i zamraża pozostałe.
Object.keys(obj).
forEach(name = deepFreeze(obj[name]));
Object.freeze(obj);
}
return obj;
}
Rekurencyjnie wywołuje
samą siebie (rekurencję
omawiam w rozdziale 3.).
Zamraża główny obiekt.
Pobiera wszystkie
właściwości i rekurencyjnie
wywołuje funkcję
Object.freeze() dla tych,
które nie są jeszcze
zamrożone (funkcję map
omawiam w rozdziale 3.).
Przedstawiłem tu wybrane techniki, które można zastosować, aby zapewnić odpowiedni
poziom niemodyfikowalności w kodzie. Nierealne jest jednak oczekiwanie, że można
tworzyć całe aplikacje bez modyfikowania stanu. Dlatego ścisłe reguły tworzenia nowych
obiektów na podstawie pierwotnych (tak jak za pomocą wywołania coordinate.trans
late()) są bardzo przydatne, jeśli chcesz ograniczyć złożoność i zawiłość aplikacji
w JavaScripcie. Dalej omawiam najlepszy sposób scentralizowanego zarządzania zmia-
nami obiektów z zachowaniem niemodyfikowalności za pomocą funkcyjnej techniki
o nazwie soczewki (ang. lenses).
2.2.4. Poruszanie się po grafach obiektów
i ich modyfikowanie za pomocą soczewek
W programowaniu obiektowym programiści przyzwyczajeni są do wywoływania metod,
które zmieniają wewnętrzną zawartość stanowych obiektów. Wadą tego podejścia jest
to, że nie można zagwarantować, jaki skutek będzie miało pobranie stanu. Ponadto
fragmenty systemu oczekujące, że obiekt nie będzie się zmieniał, mogą wtedy działać
nieprawidłowo. Możesz zdecydować się na strategię kopiowania przy zapisie i zwracać
nowe obiekty dla każdego wywołania metody. Jest to jednak żmudna i narażona na błędy
technika (delikatnie mówiąc). Prosty setter w klasie Person wyglądałby wtedy tak:
set lastname(lastname) {
return new Person(this._firstname, lastname, this._ssn);
};
Musiałbyś ręcznie
kopiować stan wszystkich
właściwości do nowego
obiektu (to okropne
rozwiązanie).
Teraz wyobraź sobie, że podobne zabiegi musisz stosować do każdej właściwości każ-
dego typu w modelu dziedziny. Potrzebny jest sposób zmieniania obiektów stanowych
z zachowaniem niemodyfikowalności, bez nadmiernej ingerencji w rozwiązanie i bez
konieczności pisania za każdym razem szablonowego kodu. Soczewki (nazywane też
referencjami funkcyjnymi; ang. functional references) to dostępne w programowaniu
funkcyjnym rozwiązanie problemu dostępu do atrybutów stanowych typów danych
i operowania nimi z zachowaniem niemodyfikowalności. Wewnętrznie soczewki dzia-
łają podobnie jak strategia kopiowania przy zapisie. Używany jest wewnętrzny kom-
ponent do obsługi przechowywania stanu, który potrafi poprawnie zarządzać stanem
i go kopiować. Nie musisz samodzielnie pisać tego komponentu. Wystarczy zastosować
funkcyjną bibliotekę JavaScript Ramda.js (szczegółowe informacje o niej i o innych
bibliotekach znajdziesz w dodatku). Ramda domyślnie udostępnia wszystkie możliwości
za pomocą globalnego obiektu R. Za pomocą wywołania R.lensProp możesz utworzyć
soczewkę, która stanowi nakładkę na właściwość lastname z typu Person:
Poleć książkęKup książkę2.2. Programowanie funkcyjne a programowanie obiektowe
55
var person = new Person( Alonzo , Church , 444-44-4444 );
var lastnameLens = R.lenseProp( lastName );
Wywołanie R.view pozwala wczytać zawartość tej właściwości:
R.view(lastnameLens, person); //- Church
W praktyce to rozwiązanie działa podobnie jak metoda get lastname(). Nie ma w tym
nic nadzwyczajnego. A co z setterem? Tu ujawniają się możliwości biblioteki. Wywoła-
nie R.set tworzy i zwraca nowy egzemplarz obiektu zawierający nową wartość i zacho-
wujący stan pierwotnego obiektu. Otrzymujesz więc „za darmo” mechanizm kopiowania
przy zapisie:
var newPerson = R.set(lastnameLens, Mourning , person);
newPerson.lastname; //- Mourning
person.lastname; //- Church
Soczewki są przydatne, ponieważ zapewniają niewymagający ingerencji w kod mecha-
nizm operowania obiektami — nawet gdy są to istniejące już obiekty lub obiekty poza
kontrolą programisty. Soczewki obsługują też właściwości zagnieżdżone, takie jak wła-
ściwość address z typu Person:
person.address = new Address(
USA , NJ , Princeton , zipCode( 08544 , 1234 ),
Alexander St. );
Utwórzmy teraz soczewkę dla właściwości address.zip:
var zipPath = [ address , zip ];
var zipLens = R.lens(R.path(zipPath), R.assocPath(zipPath));
R.view(zipLens, person); //- zipCode( 08544 , 1234 )
Ponieważ soczewki tworzą settery zachowujące niemodyfikowalność, to po zmodyfi-
kowaniu zagnieżdżonego obiektu można zwrócić nowy obiekt:
var newPerson = R.set(zipLens, zipCode( 90210 , 5678 ), person);
var newZip = R.view(zipLens, newPerson); //- zipCode( 90210 , 5678 )
var originalZip = R.view(zipLens, person); //- zipCode( 08544 , 1234 )
newZip.toString() !== originalZip.toString(); //- true
Jest to świetne rozwiązanie, ponieważ otrzymujesz gettery i settery działające w funk-
cyjny sposób. Soczewki nie tylko zapewniają nakładkę zachowującą niemodyfikowal-
ność, ale też doskonale wpisują się w filozofię programowania funkcyjnego, ponieważ
oddzielają logikę dostępu do pól od samego obiektu. Eliminuje to zależność od obiektu
this i daje przydatne funkcje, które potrafią dotrzeć do zawartości dowolnego obiektu
i operować nią.
Definiuje działanie
gettera i settera.
Gdy już wiesz, jak poprawnie pracować z obiektami, pora zmienić temat i przyj-
rzeć się funkcjom. To właśnie one sterują pracą aplikacji i są istotą programowania
funkcyjnego.
Poleć książkęKup książkę56
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
2.3. Funkcje
W programowaniu funkcyjnym to funkcje są podstawową jednostką pracy. To oznacza,
że wszystko związane jest właśnie z nimi. Funkcja to dowolne wywoływalne wyrażenie,
które można przetworzyć za pomocą operatora (). Funkcje mogą zwracać do jed-
nostki wywołującej albo obliczoną wartość, albo wynik undefined (dotyczy to funkcji
bez zwracanej wartości). Ponieważ programowanie funkcyjne działa w matematyczny
sposób, funkcje mają znaczenie tylko wtedy, gdy generują możliwy do użycia wynik
(różny od null lub undefined). W przeciwnym razie należy założyć, że funkcja mody-
fikuje zewnętrzne dane i powoduje efekty uboczne. Na potrzeby tej książki rozróżniam
wyrażenia (funkcje generujące wynik) i instrukcje (funkcje, które nie generują wyniku).
Platformy imperatywne i proceduralne składają się głównie z uporządkowanych sekwen-
cji instrukcji. Jednak programowanie funkcyjne jest oparte na wyrażeniach, dlatego
funkcje bez zwracanej wartości nie są w tym paradygmacie przydatne.
Funkcje w JavaScripcie mają dwie cechy typowe dla stylu funkcyjnego: są pełno-
prawnymi obiektami i mogą być funkcjami wyższego poziomu. Dalej szczegółowo
omawiam oba te aspekty.
2.3.1. Funkcje jako pełnoprawne obiekty
W JavaScripcie pojęcie pełnoprawne obiekty (ang. first-class citizens) związane jest
z tym, że funkcje działają w nim jak obiekty. Prawdopodobnie przyzwyczaiłeś się do
funkcji deklarowanych w następujący sposób:
function multiplier(a,b) {
return a * b;
}
Jednak JavaScript udostępnia też inne możliwości. Oto co można zrobić z funkcjami
(podobnie jak z obiektami):
Przypisywać do zmiennych jako funkcje anonimowe lub wyrażenia lambda
(szczegółowe omówienie lambd przedstawiam w rozdziale 3.):
var square = function (x) {
return x * x;
}
Funkcja
anonimowa
var square = x = x * x;
Wyrażenie lambda
Przypisywać do właściwości obiektów jako metody:
var obj = {
method: function (x) { return x * x; }
};
W wywołaniach funkcji używany jest operator (), na przykład square(2), natomiast
obiekt reprezentujący funkcję jest wyświetlany w następujący sposób:
square;
// function (x) {
// return x * x;
// }
Poleć książkęKup książkę2.3. Funkcje
57
Choć nie jest to powszechnie stosowane rozwiązanie, funkcje można tworzyć także za
pomocą konstruktorów. Dowodzi to, że w JavaScripcie funkcje to pełnoprawne obiekty.
Konstruktor przyjmuje zbiór parametrów formalnych i ciało funkcji, a wywoływany jest
za pomocą słowa kluczowego new:
var multiplier = new Function( a , b , return a * b );
multiplier(2, 3); //- 6
W JavaScripcie każda funkcja to obiekt typu Function. Właściwość length funkcji pozwala
pobrać liczbę parametrów formalnych, a metody apply() i call() można stosować do
wywoływania funkcji z użyciem kontekstu. Więcej na temat tych metod dowiesz się
z następnego punktu.
Po prawej stronie wyrażenia z funkcją anonimową znajduje się obiekt reprezentu-
jący funkcję z pustą właściwością name. Za pomocą funkcji anonimowych przekazywa-
nych jako argument możesz tworzyć rozszerzone lub wyspecjalizowane wersje funkcji.
Zastanów się nad natywną funkcją Array.sort(comparator) z JavaScriptu. Przyjmuje
ona obiekt reprezentujący funkcję porównującą wartości. Domyślnie funkcja sort prze-
kształca sortowane wartości na łańcuchy znaków i używa ich wartości z kodowania
Unicode jako naturalnego kryterium sortowania. Ma to pewne ograniczenia i często nie
jest zgodne z potrzebami programisty. Przyjrzyj się kilku przykładom:
var fruit = [ Cebula , ananas ];
fruit.sort(); //- [ Cebula , ananas ]
W kodowaniu Unicode wielkie litery
znajdują się przed małymi.
Liczby są przekształcane na łańcuchy znaków i porównywane
zgodnie z ich wartościami w kodowaniu Unicode.
var ages = [1, 10, 21, 2];
ages.sort(); //- [1, 10, 2, 21]
Tak więc sort() to funkcja, której działanie często zależy od kryteriów określonych
w funkcji comparator. Sama w sobie funkcja sort() jest prawie bezużyteczna. Możesz
wymusić poprawne porównywanie na podstawie liczb i posortować listę osób według
wieku, podając jako argument niestandardową funkcję:
people.sort((p1, p2) = p1.getAge() p2.getAge());
Funkcja comparator przyjmuje tu dwa parametry, p1 i p2, i działa zgodnie z następują-
cym kontraktem:
jeśli comparator zwraca wartość mniejszą niż 0, p1 ma znajdować się przed p2;
jeśli comparator zwraca wartość 0, należy zachować pozycje p1 i p2;
jeśli comparator zwraca wartość większą niż 0, p1 ma znajdować się po p2.
Oprócz tego, że funkcje można przypisywać do zmiennych, niektóre funkcje JavaScriptu
(na przykład sort()) przyjmują jako argumenty inne funkcje i należą do grupy tak zwa-
nych funkcji wyższego poziomu.
2.3.2. Funkcje wyższego poziomu
Ponieważ funkcje działają jak zwykłe obiekty, zgodne z intuicją jest założenie, że
można je przekazywać jako argumenty i zwracać w innych funkcjach. Te „inne funkcje”
to funkcje wyższego poziomu. Poznałeś już funkcję comparator w funkcji Array.sort().
Przyjrzyj się teraz pokrótce innym przykładom.
Poleć książkęKup książkę58
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
W poniższym przykładzie pokazuję, że funkcje można przekazywać do innych funk-
cji. Funkcja applyOperation przyjmuje dwa argumenty i stosuje do nich funkcję opt:
function applyOperation(a, b, opt) {
return opt(a,b);
}
Funkcję opt() można przekazywać
jako argument do innych funkcji.
var multiplier = (a, b) = a * b;
Funkcja jest zwracana
przez inną funkcję.
applyOperation(2, 3, multiplier); // - 6
W następnym przykładzie funkcja add przyjmuje argument i zwraca funkcję, która
przyjmuje drugi argument i zwraca sumę obu argumentów:
function add(a) {
return function (b) {
return a + b;
}
}
add(3)(3); //- 6
Ponieważ funkcje to pełnoprawne obiekty i można tworzyć funkcje wyższego poziomu,
w JavaScripcie funkcje mogą działać jak wartości. Z tego wynika, że funkcja to przezna-
czona do wykonania wartość zdefiniowana z uwzględnieniem niemodyfikowalności
na podstawie przekazywanych do niej danych wejściowych. Ta reguła jest wbudowana
w programowanie funkcyjne, a zwłaszcza w łańcuchy funkcji, o czym przekonasz się
w rozdziale 3. Gdy tworzysz łańcuchy funkcji, zawsze korzystasz z nazw funkcji do wska-
zywania fragmentu programu, który zostanie wykonany w ramach całego wyrażenia.
Funkcje wyższego poziomu można łączyć ze sobą, aby tworzyć sensowne wyrażenia
na podstawie mniejszych fragmentów kodu i upraszczać programy, których pisanie
w innym paradygmacie byłoby żmudne. Oto przykład: załóżmy, że chcesz wyświetlić
listę osób mieszkających w Stanach Zjednoczonych. Twój pierwszy pomysł może wyglą-
dać tak jak w pokazanym poniżej kodzie imperatywnym:
function printPeopleInTheUs(people) {
for (let i = 0; i people.length; i++) {
var thisPerson = people[i];
if(thisPerson.address.country === USA ) {
console.log(thisPerson);
}
}
}
printPeopleInTheUs([p1, p2, p3]);
Teraz załóżmy, że chcesz dodać obsługę wyświetlania osób mieszkających w innych
państwach. Za pomocą funkcji wyższego poziomu możesz przedstawić w abstrakcyj-
nej postaci operacje wykonywane na każdej osobie (tu jest to wyświetlanie informacji
o osobie w konsoli). Możesz swobodnie przekazać dowolną funkcję action do funkcji
wyższego poziomu printPeople:
function printPeople(people, action) {
for (let i = 0; i people.length; i++) {
action (people[i]);
Wywołuje metodę toString
każdego obiektu.
p1, p2 i p3 to obiekty
typu Person.
Poleć książkęKup książkę2.3. Funkcje
59
}
}
var action = function (person) {
if(person.address.country === USA ) {
console.log(person);
}
}
printPeople(people,action);
W językach takich jak JavaScript widoczny jest wzorzec, zgodnie z którym nazwami
funkcji mogą być rzeczowniki takie jak multiplier, comparator i action. Ponieważ funkcje
są pełnoprawnymi obiektami, można je przypisywać do zmiennych i wywoływać póź-
niej. Przekształć funkcję printPeople, aby w pełni wykorzystać możliwości funkcji wyż-
szego poziomu:
function printPeople(people, selector, printer) {
people.forEach(function (person) {
if(selector(person)) {
printer(person);
}
});
}
Wywołanie forEach to preferowany sposób
tworzenia pętli zgodny ze stylem funkcyjnym.
Omawiam tę kwestię w dalszej części rozdziału.
mnieć o tym wyjątkowym aspekcie mechanizmu uruchamiania funkcji w JavaScripcie.
2.3.3. Sposoby uruchamiania funkcji
Mechanizm uruchamiania funkcji w JavaScripcie to ciekawy aspekt tego języka, dzia-
łający inaczej niż w innych językach. JavaScript daje programiście pełną swobodę
w określaniu kontekstu, w którym funkcja ma być uruchamiana. Kontekst ten jest
określany za pomocą słowa this w ciele funkcji. Funkcje w JavaScripcie można uru-
chamiać na wiele sposobów:
Jako funkcje globalne. W tym modelu referencja this prowadzi do obiektu
globalnego albo ma wartość undefined (w trybie strict):
Dzięki użyciu funkcji wyższego
poziomu widoczny staje się
deklaratywny charakter kodu.
To wyrażenie jednoznacznie określa,
co program robi.
var inUs = person = person.address.country === USA ;
printPeople(people, inUs, console.log);
Takie nastawienie powinieneś w sobie rozwijać, aby w pełni wykorzystać programo-
wanie funkcyjne. Zaprezentowane ćwiczenie pokazuje, że nowa wersja kodu jest dużo
bardziej elastyczna niż początkowe rozwiązanie. Jest tak, ponieważ można szybko zmie-
nić (lub skonfigurować) kryteria wyboru osób, a także określić miejsce, gdzie dane mają
trafić. W rozdziałach 3. i 4. koncentruję się na tym zagadnieniu i przedstawiam specjalne
biblioteki pozwalające płynnie łączyć operacje w łańcuchy oraz tworzyć złożone pro-
gramy z prostych komponentów.
W JavaScripcie funkcje są nie tylko wywoływane, ale też stosowane. Warto wspo-
Poleć książkęKup książkę60
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
Wybiegając naprzód…
Przerywam na chwilę omawianie podstaw związanych z JavaScriptem, aby dokładniej opi-
sać program z tego punktu i połączyć niektóre pokrótce poruszone zagadnienia. Na razie
mogą wydać Ci się one skomplikowane, jednak szybko nauczysz się budować programy
w przedstawiony tu sposób za pomocą technik funkcyjnych. Przy użyciu soczewek możesz
utworzyć funkcje pomocnicze i uzyskać dzięki nim dostęp do właściwości obiektu:
var countryPath = [ address , country ];
var countryL = R.lens(R.path(countryPath), R.assocPath(countryPath));
var inCountry = R.curry((country, person) =
R.equals(R.view(countryL, person), country));
Poniższe wywołanie jest dużo bardziej funkcyjne niż wcześniejsza wersja kodu:
people.filter(inCountry( USA )).map(console.log);
Nazwa państwa jest tu następnym parametrem, który można dowolnie zmieniać. W dal-
szych rozdziałach znajdziesz więcej kodu działającego w podobny sposób.
function doWork() {
this.myVar = Jakaś wartość ;
}
doWork();
Wywołanie doWork() na poziomie
globalnym powoduje, że referencja this
prowadzi do obiektu globalnego.
Jako metody. Referencja this jest ustawiana na jednostkę, do której metoda
należy. Jest to ważny aspekt obiektowej natury JavaScriptu:
var obj = {
prop: Jakaś właściwość ,
getProp: function () {return this.prop}
};
obj.getProp();
Wywołanie metody obiektu sprawia,
że this prowadzi do tego obiektu.
Jako konstruktor (wywołanie jest wtedy poprzedzone słowem new). Takie wywo-
łanie niejawnie zwraca referencję do nowo utworzonego obiektu:
function MyType(arg) {
this.prop = arg;
}
Wywołanie funkcji z użyciem słowa new
sprawia, że this prowadzi do tworzonego
i niejawnie zwracanego obiektu.
var someVal = new MyType( Jakiś argument );
Te przykłady pokazują, że (inaczej niż w innych językach programowania) obiekt
wskazywany przez this zależy od sposobu uruchomienia funkcji (globalnie, jako me-
tody obiektu, jako konstruktora itd.), a nie od jej kontekstu leksykalnego (czyli od
miejsca użycia w kodzie). Może to prowadzić do trudnego do zrozumienia kodu, po-
nieważ trzeba zwracać baczną uwagę na kontekst wykonywania funkcji.
Dodałem ten fragment, ponieważ programiści używający JavaScriptu powinni
znać ten materiał. Jednak w kodzie funkcyjnym, o czym już kilkakrotnie wspomniałem,
referencja this jest spotykana rzadko (należy jej unikać za wszelką cenę). Natomiast
często posługują się nią autorzy bibliotek i narzędzi w specjalnych sytuacjach, które
wymagają modyfikacji kontekstu języka w celu wykonania zaskakujących sztuczek.
Te sztuczki nieraz obejmują wywołania używanych dla funkcji metod apply i call.
Poleć książkęKup książkę2.3. Funkcje
61
2.3.4. Metody używane dla funkcji
JavaScript umożliwia uruchamianie funkcji za pomocą przeznaczonych dla nich metod
call i apply (możesz traktować je jak metafunkcje), należących do prototypu funkcji.
Obie te metody są często używane w kodzie rusztowania (ang. scaffolding), co pozwala
użytkownikom interfejsu API tworzyć nowe funkcje na podstawie istniejących. Zobacz,
jak można napisać funkcję negate:
Tworzenie funkcji wyższego poziomu o nazwie negate.
Przyjmuje ona funkcję wejściową i zwraca funkcję
function negate(func) {
negującą wynik tej pierwszej.
return function() {
return !func.apply(null, arguments);
};
}
Użycie wywołania Function.apply()
w celu uruchomienia funkcji wejściowej
z pierwotnymi argumentami.
function isNull(val) {
return val === null;
}
Definicja funkcji isNull.
var isNotNull = negate(isNull);
Definicja funkcji isNotNull jako negacji funkcji isNull.
isNotNull(null); //- false
isNotNull({}); //- true
Funkcja negate tworzy nową funkcję, która wykonuje funkcję wejściową z jej argumen-
tami i neguje jej wynik. W tym przykładzie używana jest metoda apply, w ten sam
sposób możesz jednak zastosować także metodę call. Różnica polega na tym, że call
przyjmuje listę argumentów, natomiast apply — tablicę argumentów. Pierwszy argu-
ment, thisArg, pozwala w razie potrzeby zmodyfikować kontekst funkcji. Oto sygna-
tury obu omawianych metod:
Function.prototype.apply(thisArg, [argsArray])
Function.prototype.call(thisArg, arg1,arg2,...)
Jeśli thisArg prowadzi do obiektu, jest nim obiekt, dla którego wywoływana jest dana
metoda. Jeżeli thisArg to null, kontekstem funkcji jest obiekt globalny, a funkcja działa
jak prosta funkcja globalna. Gdy jednak używany jest tryb strict, przekazywana jest
sama wartość null.
Modyfikowanie kontekstu funkcji za pomocą argumentu thisArg pozwala stosować
wiele różnych technik. Jednak w programowaniu funkcyjnym używanie tego argumentu
nie jest zalecane, ponieważ nie należy polegać na stanie kontekstu (pamiętaj, że wszyst-
kie dane są przekazywane do funkcji jako argumenty). Z tego powodu nie omawiam
więcej tego mechanizmu.
Choć współużytkowany kontekst globalny i kontekst obiektu nie odgrywają w funk-
cyjnym JavaScripcie dużego znaczenia, jeden kontekst jest istotny. Chodzi tu o kon-
tekst funkcji. Aby go zrozumieć, trzeba zapoznać się z domknięciami i zasięgiem.
Poleć książkęKup książkę62
ROZDZIAŁ 2. Operacje wyższego poziomu w JavaScripcie
2.4. Domknięcia i zasięg
Przed pojawieniem się JavaScriptu domknięcia występowały tylko w językach funk-
cyjnych używanych w pewnych specyficznych zastosowaniach. W JavaScripcie po raz
pierwszy pojawiły się one w standardowych zastosowaniach i znacznie zmieniły sposób
pisania kodu. Wróćmy do typu zipCode:
function zipCode(code, location) {
let _code = code;
let _location = location || ;
return {
code: function () {
return _code;
},
location: function () {
return _location;
},
...
};
}
Gdy przeanalizujesz ten kod, zauważysz, że funkcja zipCode zwraca literał obiektowy,
który najwyraźniej ma pełny dostęp do zmiennych zadeklarowanych poza jego zasię-
giem. Oznacza to, że po zakończeniu pracy przez funkcję zipCode wynikowy obiekt
nadal ma dostęp do zadeklarowanych w niej informacji:
const princetonZip = zipCode( 08544 , 3345 );
princetonZip.code(); //- 08544
Trudno jest to zrozumieć. Technika ta działa dzięki domknięciu tworzonemu w Java-
Scripcie wokół deklaracji obiektu i funkcji. Możliwość dostępu do danych w ten spo-
sób ma wiele praktycznych zastosowań. W tym podrozdziale pokazuję, jak za pomocą
domknięć zasymulować tworzenie zmiennych prywatnych, pobierać dane z serwera
i tworzyć zmienne o zasięgu ograniczonym do danego bloku.
Domknięcie to struktura danych, która wiąże funkcję z jej środowiskiem w momencie
jej deklaracji. Technika ta oparta jest na lokalizacji tekstu deklaracji funkcji. Dlatego
domknięcie można nazwać statycznym lub leksykalnym zasięgiem otaczającym definicję
funkcji. Ponieważ zapewnia to funkcji dostęp do otaczającego ją stanu, kod jest przej-
rzysty i czytelny. Wkrótce zobaczysz, że domknięcia są bardzo istotne nie tylko w pro-
gramach funkcyjnych (gdzie używane są funkcje wyższego poziomu), ale też do obsługi
zdarzeń i wywołań zwrotnych, symulowania zmiennych prywatnych i radzenia sobie
z niektórymi pułapkami z JavaScriptu.
Reguły rządzące działaniem domknięć są ściśle związane z regułami określania
zasięgu w JavaScripcie. Zasięg grupuje zbiór wiązań zmiennych i definiuje fragment
kodu, w którym dana zmienna jest zdefiniowana. Domknięcie to wynik dziedziczenia
zasięgu przez funkcję. Podobnie metody obiektu mają dostęp do odziedziczonych zmien-
nych instancji. W obu przypadkach używane są referencje do jednostki nadrzędnej.
Domknięcia często występują przy stosowaniu funkcji zagnieżdżon
Pobierz darmowy fragment (pdf)