Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00066 006356 11244831 na godz. na dobę w sumie
Programowanie funkcyjne z JavaScriptem. Sposoby na lepszy kod - ebook/pdf
Programowanie funkcyjne z JavaScriptem. Sposoby na lepszy kod - ebook/pdf
Autor: Liczba stron: 256
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-3253-9 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> webmasterstwo >> javascript - programowanie
Porównaj ceny (książka, ebook (-20%), audiobook).

Każdy paradygmat programowania zakłada inne podejście do rozwiązywania problemów. Mimo że podejście obiektowe wciąż jest podstawowym modelem projektowania programowania, podejście funkcyjne pozwala na uzyskanie kodu lepszej jakości: modularnego, ekspresywnego, odpornego na błędy, a przy tym zrozumiałego i łatwego w testowaniu. Szczególnie interesujące jest stosowanie w modelu funkcyjnym języka JavaScript. Chociaż jest to język obiektowy, okazuje się, że taki sposób programowania pozwala na uzyskiwanie wyjątkowo efektywnego i elastycznego kodu.

Niniejsza książka jest przeznaczona dla programistów, którzy chcą się nauczyć programowania funkcyjnego w JavaScripcie. Przedstawiono tu zarówno teoretyczne aspekty tego paradygmatu, jak i konkretne mechanizmy: funkcje wyższego poziomu, domknięcia, rozwijanie funkcji, kompozycje. Nieco trudniejszymi zagadnieniami, które tu omówiono, są monady i programowanie reaktywne. Ten poradnik pozwala też zrozumieć zasady tworzenia asynchronicznego kodu sterowanego zdarzeniami i w pełni wykorzystać możliwości JavaScriptu.

W książce omówiono:

Programowanie funkcyjne — i kod staje się lepszy!


Luis Atencio — jest inżynierem oprogramowania. Zajmuje się tworzeniem architektury aplikacji dla różnych przedsiębiorstw. Tworzy kod w JavaScripcie, Javie i PHP. Jest osobą o dużym talencie do przekazywania wiedzy. Bardzo często dzieli się swoimi doświadczeniami podczas konferencji branżowych. Prowadzi blog na temat inżynierii oprogramowania i pisze artykuły dla rozmaitych magazynów oraz serwisu DZone.

Znajdź podobne książki Ostatnio czytane w tej kategorii

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)

Gdzie kupić całą publikację:

Programowanie funkcyjne z JavaScriptem. Sposoby na lepszy kod
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: