Budowanie za pomocą pierwotnych elementów funkcyjnych 248
Prosta animacja w grze 249
O autorach
Bibliografia
Indeks .
ROZDZIAŁ 1
Programowanie funkcyjne wraca do łask
Michael Swaine
Lato, gdy przeniosłem się do Doliny Krzemowej, było tym latem, w którym zlikwidowano ILLIAC IV w Moffett Field.
Widzieliśmy już ten film
W 1981 roku centrum zainteresowania informatyki przeniosło się z ogromnych maszyn obsługiwanych przez kapłanów w białych fartuchach na tanie komputery osobiste tworzone i programowane przez niechlujnych hakerów. Ja przeniosłem się ze środkowego zachodu do Palo Alto i zgłosiłem się do pomocy w utworzeniu nowego tygodnika obsługującego tych niechlujów. W międzyczasie, w niewielkiej odległości – w ośrodku NASA Moffett Field oficjalnie zamknięto i rozłożono na części komputer, który zainspirował Stanleya Kubricka i Arthura C. Clarke’a do wymyślenia komputera HAL 9000.
ILLIAC IV to legendarny punkt zwrotny w projektowaniu komputerów, mający podobną pozycję w dł ugiej i skomplikowanej historii programowania funkcyjnego.
Koncepcją leżącą u podstaw ILLIAC IV było oderwanie się od modelu sekwencyjnego, który od początku dominował w informatyce. Pewne obszary problemów, takie jak mechanika płynów, lepiej nadawały się do przetwarzania równoległego, a ILLIAC IV został specjalnie zaprojektowany dla tego rodzaju równoległych problemów – takich, w których jedna instrukcja mogła zostać zastosowana równolegle do wielu zbiorów danych. Jest to znane pod skrótem SIMD (single instr uction, multiple data – jedna instrukcja, wiele danych). Bardziej ogólny przypadek – MIMD (multiple instructions operating on multiple data
Rozdział 1. Programowanie funkcyjne wraca do łask • 4
sets – wiele instrukcji działających na wielu zbiorach danych) jest trudniejszy. Ale każdy rodzaj równoległego programowania wymaga nowych algorytmów i nowego podejścia. I stanowi zaproszenie do programowania funkcyjnego.
Programowanie funkcyjne, określane skrótem FP, zgodnie z Wikipedią „traktuje obliczenia jako wyznaczanie matematycznych wartości funkcyjnych i unika zmiany stanu i zmiany danych”. Może to posłużyć za podstawową definicję, ale my musimy się zagłębić dużo bardziej.
FP funkcjonuje od lat pięćdziesiątych XX wieku, gdy John McCarthy wymyślił język Lisp, ale dążenie do przetwarzania równoległego dało programowaniu funkcyjnemu nowy impet. Unikanie zmiany stanu było wtedy trudną do przełknięcia pigułką, ale pasowało do modelu SIMD. To doprowadziło do rozwoju nowych języków i nowych cech FP w istniejących językach. Fortran był wtedy językiem, w którym większość wykonywała obliczenia naukowe, więc naturalne było jego rozszerzanie. Programiści ILLIAC IV mogli pisać programy w IVTRAN, TRANQUIL lub CFD, zrównoleglonych wersjach FORTRAN-u.
Powstała też zrównoleglona wersja języka ALGOL.
Dla odpowiednich typów problemów, które można rozwiązać za pomocą SIMD, ILLIAC IV był najszybszym komputerem na świecie i utrzymał ten tytuł do czasu jego wycofania z eksploatacji w 1981 roku. Był o rząd wielkości szybszy od każdego komputera w tamtych czasach i był doskonale dopasowany do swoich docelowych zastosowań i programowania funkcyjnego.
Ale era ta gwałtownie się skończyła. 7 września 1981 roku ILLIAC IV został na dobre zatrzymany. Można dziś zobaczyć jego fragment w muzeum historii komputerów w Mountain View, niedaleko miejscowości Moffett Field.
Dlaczego ta era dobiegła końca? Według Wikipedii: „Illiac IV należał do klasy komputerów równoległych, określanych jako SIMD. Prawo Moore’a prześcignęło specjalizowane podejście stosowane w SIMD ILLIAC, sprawiając, że podejście
MIMD stało się preferowane dla niemal wszystkich obliczeń naukowych”.
Po drodze zyskał jeszcze jedno miejsce w historii – inspirację dla komputera
HAL 9000 w filmie 2001: Odyseja kosmiczna. Arthur C. Clarke nie był neofitą w kwestii komputerów – rozmawiał z Turingiem w Bletchley Park i pilnie śledził postępy w dziedzinie mikrokomputerów. Clarke był zaintrygowany, gdy dowiedział się o działaniu ILLIAC IV na Uniwersytecie Stanu Illinois w Urbana-Champaign i uczcił ten kampus w swoim filmie jako miejsce narodzin
HAL-a 9000.
Przejdźmy do roku 2000. Zastosowanie, które sprawiło, że FP stało się warte nauki, okazało się znowu przetwarzaniem równoległym, ale napędzanym pojawieniem się procesorów wielordzeniowych. „Twórcy chipów”, jak wtedy twierdziłem, „mówili w istocie, że wdrażanie prawa Moore’a to teraz domena
oprogramowania. Oni skoncentrują się na wstawianiu coraz więcej rdzeni do kości, a programiści muszą tak przerobić swoje programy, aby wykorzystać możliwości przetwarzania równoległego w ich układach”.
Impet, jaki zyskało FP na początku XXI wieku, wynikał z chęci oderwania się od modelu sekwencyjnego, przy czym zaoferowane zostały różne podejścia.
Osoby, które zainwestowały wiele w umiejętności i narzędzia w Javie, nie chciały odkładać na bok bibliotek kodów, umiejętności i narzędzi, na których się opierały, mogły używać języka Scala Martina Odersky’ego, ostatnio wprowadzonego na platformie Javy. Zaoferowano go też na platformie .NET, choć (niestety) zaniechano jego wsparcia w 2012 roku. Użytkownicy .NET powinni raczej przymierzać się do F#, utworzonego przez Dona Syme’a z Microsoft Research. Kto chce mieć czysto funkcyjny język, może skorzystać z Erlang, opracowanego dla wysoce równoległego programowania przez Joe Ar mstronga z Ericssona, z języka Haskell lub wielu innych możliwości.
Tym, co oferują wszystkie te języki, jest możliwość pracy w paradygmacie funkcyjnym. Dwie podstawowe cechy paradygmatu funkcyjnego to potraktowanie wszystkich obliczeń jako wyznaczania wartości funkcyjnych oraz unikanie zmiany stanu i mutowalnych danych. Ale są też inne wspólne cechy programowania funkcyjnego:
• funkcje pierwszoklasowe – funkcje mogą służyć jako argumenty i wyniki funkcji;
• rekurencja jako podstawowe narzędzie iteracji;
• szerokie stosowanie dopasowywania do wzorców;
• leniwe wartościowanie, co pozwala na tworzenie nieskończonych ciągów. Programiści iteracyjni, którzy po raz pierwszy stykają się z FP, mogą jeszcze dodać – jest ono wolne i niejasne. W istocie żadna z tych opinii nie występuje zawsze, ale wymaga to zmiany sposobu myślenia o problemach i różnych algorytmach. Aby programy działały odpowiednio, muszą mieć lepsze wsparcie ze strony języka niż to, które było powszechne w pierwszej dekadzie XXI wieku. To się zmieni w kolejnym dziesięcioleciu, a wraz z tym zmienią się przyczyny, dla któr ych FP przyciąga ludzi do siebie.
Nowe argumenty za programowaniem funkcyjnym
Po upływie dekady ponownie wróciła sprawa programowania funkcyjnego.
Pojawiło się nowe wsparcie językowe i sprawa FP stała się szersza.
Chociaż równoległość była tradycyjnie siłą napędową programowania funkcyjnego, teraz gdy mówi się o FP, częściej odwołuje się do zdolności spojrzenia na problem na wyższym poziomie oraz o zaletach niemutowalności. Zamiast
Rozdział 1. Programowanie funkcyjne wraca do łask • 6
patrzeć na FP jak na dziwny sposób, który trzeba zastosować, aby poradzić sobie z równoległością, nowym argumentem jest bardziej naturalny sposób pisania, bliższy pojęciom i terminologii dla naszej dziedziny. Ludzie używają FP do aplikacji, które nie wymagają równoległości, aby programować bardziej efektywnie i jasno, działając bliżej sposobu myślenia o swoich problemach.
Mówi się, że zamiast konieczności przełożenia problemu na język programowania, adaptujemy język do problemu.
A jeśli mamy dostępnych wiele rdzeni i równoległość może być korzystna dla naszego kodu, to mamy równoległość za darmo. Neal Ford powiedział: „Ostatnie mądre innowacje w bibliotekach [Clojure] pozwoliły przepisać funkcję odwzorowania tak, aby automatycznie stawała się równoległa, co oznacza, że wszystkie działania związane z odwzorowaniami skorzystają z przyspieszenia wydajności bez inter wencji dewelopera”.
Dobre dopasowanie FP do współbieżności przemawia do ludzi, którzy piszą aplikacje wieloprocesorowe, aplikacje o dużej dostępności, serwery WWW dla sieci społecznościowych i do wielu innych zastosowań. Wyższy poziom abstrakcji FP przemawia do osób, które szukają krótszego czasu pracy dewelopera lub lepiej zrozumiałego kodu. FP kładzie nacisk na niemutowalność, co silnie przemawia do każdego, kto jest zainteresowany niezawodnością.
Wzrost sprzedaży stanowi prawdziwą zmianę z argumentacji na rzecz programowania funkcyjnego z czasów ILLIAC IV. Dziś są nowe powody, aby spojrzeć na FP, a także lepsze wsparcie językowe FP i większy wybór podejść. Zaczyna się to od wyboru języka. Można wygodnie trzymać się znanych sobie języków i ich narzędzi, wprowadzając funkcyjność tam, gdzie jest dla nas użyteczna. Lub też można przejść do języka zbudowanego od podstaw do programowania funkcyjnego. W tej książce zobaczymy oba te podejścia.
To ekscytujący czas na poznawanie programowania funkcyjnego, a korzyści z niego wynikające są znaczne. Ale wymaga to jednak innego sposobu myślenia.
W następnym rozdziale Michael Bevilacqua-Linn zaprasza nas do myślenia o programowaniu w sposób funkcyjny.
Tworzenie funkcji wyższego rzędu w języku Scala
Venkat Subramaniam
W rozdziale 3, Scala i styl funkcyjny (s. 15), omawialiśmy funkcje wyższego rzędu w programowaniu funkcyjnym, a w rozdziale 4, Praca z kolekcjami języka Scala (s. 23), przyjrzeliśmy się funkcjom wyższego rzędu w API kolekcji Scali. W tym rozdziale dowiemy się, jak pisać własne funkcje wyższego rzędu. Funkcje wyższego rzędu mogą akceptować inne funkcje jako parametry, mogą zwracać funkcje, mogą pozwolić na tworzenie funkcji wewnątrz funkcji. W języku Scala funkcje te mogą być przekazywane i są nazywane wartościami funkcyjnymi. Wiemy, że w programowaniu obiektowym klasy (lub obiekty) abstrahują od zachowania danych i je enkapsulują. Wartości funkcji także enkapsulują zachowanie i abstrahują od niego, ale zamiast trzymać się stanu, mogą pomóc w jego przekształceniu. Popatrzmy na dwa przykłady, gdzie wartości funkcyjne stają się przydatne.
Tworzenie funkcji wyższego rzędu
Kontynuując nasz przykład giełdowy, napiszemy funkcję, która zsumuje ceny podane w kolekcji. Wydaje się, że wystarczy prosta iteracja, więc piszemy: val prices = List(10, 20, 15, 30, 45, 25, 82)
W funkcji totalAllPrices metoda foldLeft związana z listą jest używana do obliczenia sumy w stylu funkcyjnym z peł n ą niemutowalno ś ci ą . Warto ść
funkcji przekazujemy do metody foldLeft . Ta warto ść funkcji akceptuje dwa parametry i zwraca ich sumę. Metoda foldLeft wywołuje wartość funkcji tyle razy, ile jest elementów na liście. Za pierwszym razem total i price są powiązane z wartością 0 (przekazywaną jako parametr do metody foldLeft) oraz odpowiednio pierwszy element na liście. Przy drugim wywołaniu total jest powiązane z sumą zwracaną z poprzedniego wywołania
wartości funkcji, price zaś jest powiązana z drugim elementem z kolekcji. Funkcja foldLeft iteruje tę sekwencję wywołań dla pozostałych elementów z kolekcji.
Przećwiczmy naszą funkcję totalAllPrices, aby zobaczyć wynik.
println("Total of prices is " + totalAllPrices(prices)) //Total of prices is 227
Zanim możemy zadeklarować, że zostało to zrobione, otrzymujemy prośbę o napisanie jeszcze jednej funkcji, która zsumuje tylko ceny wyższe od podanej wartości. Można po prostu ponownie użyć większej części kodu z małej funkcji, którą właśnie napisaliśmy. Patrząc na zegarek (mamy umówione spotkanie), mówimy sobie „Oto dlaczego Bóg stworzył kopiuj i wklej”. W rezultacie otrzymujemy taką funkcję: def totalOfPricesOverValue(prices : List[Int], value : Int) { prices.foldLeft(0) { (total, price) => if (price > value) total + price else total } }
Niestety, zapotrzebowanie na własności wydaje się dziś niewyczerpane i za każdym razem jesteśmy proszeni o jeszcze jedną funkcję, tym razem do zsumowania tylko tych cen, które są niższe od podanej wartości. Wiemy, że kopiowanie i wklejanie kodu jest niemoralne, ale na razie się na to decydujemy, refaktorując go, aby stał się lepszy zaraz po spotkaniu, na które musimy iść.
Tuż po spotkaniu przyglądamy się kolejnej wersji swojego kodu: val prices = List(10, 20, 15, 30, 45, 25, 82)
def totalOfPricesOverValue(prices : List[Int], value : Int) = { prices.foldLeft(0) { (total, price) => if (price > value) total + price else total } }
def totalOfPricesUnderValue(prices : List[Int], value : Int) { prices.foldLeft(0) { (total, price) => if (price < value) total + price else total } }
Sprawdźmy go:
println("Total of prices is " + totalAllPrices(prices)) //Suma cen wynosi 227
println("Total of prices over 40 is " + totalOfPricesOverValue(prices, 40)) // Suma cen powyżej 40 wynosi 127
println("Total of prices under 40 is " + totalOfPricesUnderValue(prices, 40)) // Suma cen poniżej 40 wynosi 100
Mamy najlepsze intencje, aby program działał i robił to lepiej, ale chcemy go szybko refaktoryzować, aby usunąć duplikacje, zanim ktoś oskarży nas o to, że kodem tym ujawniamy naszą ciemną stronę. Mamy do ocalenia wartości funkcji.
Jeśli lekko zmodyfikujemy pierwszą funkcję do postaci if (true) total + price, zauważymy, że jedyną różnicą między treścią trzech funkcji jest wyrażenie warunkowe w instrukcji if. Możemy wyodrębnić ten warunek jako wartość funkcji.
Wyodrębniona wartość funkcji przyjmie Int jako parametr i zwróci Boolean. Możemy to wyrazić w postaci odwzorowania lub przekształcenia z Int na Boolean lub jako selector : Int => Boolean. Podobnie jak prices : List[Int] reprezentuje ceny (prices) odniesienia typu List[Int], selector : Int => Boolean reprezentuje selektor typu function value, który akceptuje Int i zwraca Boolean.
Możemy teraz zastąpić trzy poprzednie funkcje za pomocą jednej funkcji:
def totalPrices(prices : List[Int], selector : Int => Boolean) = { prices.foldLeft(0) { (total, price) => if (selector(price)) total + price else total } }
Funkcja totalPrices akceptuje kolekcję i wartość funkcji jako parametr. W funkcji, w warunku if, możemy wywołać wartość funkcji z ceną jako parametrem. Jeśli wartość funkcji selektora da wynik true, możemy dodać cenę do sumy, a w przeciwnym przypadku ignorujemy cenę.
Funkcyjne podejście do Lua
Josh Chisholm
Jeśli ktoś pisze kod w popularnym języku programowania, może nigdy nie słyszał o Lua. Ale założę się, że słyszał o Angry Birds, Wikipedii lub World of Warcraft. Co więc sprawia, że ich twórcy korzystają z Lua w tych popularnych produktach? Być może to, że Lua jest lekki, międzyplatformowy, dobrze się go osadza i rozszerza, dobrze działa, ma małe zużycie pamięci i płaską krzywą uczenia się. To dobre powody. Ale chciałbym wierzyć, że niektórych deweloperów Lua przyciąga dlatego, że obsługuje różne paradygmaty i obejmuje różne, całkiem przyjemne możliwości programowania funkcyjnego.
Prawdopodobnie znacie trochę JavaScript. Mimo że Lua ma całkiem inne pochodzenie, wiele idei projektowych ma wspólnych z JavaScript i wydaje się do niej podobna. Z punktu widzenia składni Lua jest trochę mniej kolczasta: function hello() { say("Hi there, I'm JavaScript"); } function hello() say "Howdy, I'm Lua" end
Tak jak JavaScript, Lua dużo zawdzięcza Scheme, dialektowi Lispa. Ponieważ jednak zarówno JavaScript, jak i Lua wydają się być bliżej Java i C, nie rozpoznalibyśmy szybko śladów Lisp wewnątrz nich. Gdy spojrzymy jednak na składnię, Lua ma pewne cechy, które sprawiają, że jest świetna do używania technik funkcyjnych jako część podejścia wieloparadygmatowego.
Funkcje pierwszoklasowe w Lua
Jeśli ktoś doszedł tak daleko w tej książce, wie, że powiedzenie, że funkcje są pierwszoklasowe, wskazuje tylko, że nie ma w nich nic specjalnego, więc
Rozdział 26. Funkcyjne podejście do Lua • 246
mogą być traktowane jak każda inna wartość. I wiemy, że funkcje wyższego rzędu robią przynajmniej jedną z dwóch rzeczy: albo przyjmują inne funkcje jako argumenty, albo zwracają funkcje. Są to ważne możliwości w językach funkcyjnych, zobaczmy więc, jak są one implementowane w Lua.
Przypuśćmy, że mamy podręczną listę kotów. Możemy wykorzystać koncepcję tabeli w Lua – kolekcję w stylu armii szwajcarskiej, która zachowuje się zarówno jak tablica, jak i skrót:
local cats = { { name = "meg", breed = "persian" }, { name = "mog", breed = "siamese" } }
Funkcja, która zbiera nazwy wszystkich naszych kotów, wygląda tak:
function namesOf (things) local names = {} for index, thing in pairs(things) do names[index] = thing.name end return names end
print(namesOf(cats)[1]) --> meg print(namesOf(cats)[2]) --> mog
Lua wykorzystuje indeksy (zaczynające się od 1), które są proste teoretycznie, ale można też łatwo coś popsuć.
Możemy napisać bardzo podobną funkcję breedsOf(cats), lecz musielibyśmy powtórzyć kod, aby iterować przez kolekcję rzeczy. Moglibyśmy także użyć dwukrotnie składni języka o specjalnym przeznaczeniu (for...in). Funkcje wyższego rzędu dają nam jednolitą metodę łączenia funkcji w celu zredukowania duplikacji i możemy to wykorzystać w Lua: function map (things, fn) local mapped = {} for index, thing in pairs(things) do mapped[index] = fn(thing) end return mapped end function namesOf (things) return map(things, function(thing) return thing.name end) end
print(namesOf(cats)[1]) --> meg print(breedsOf(cats)[2]) --> siamese
Dzięki takiemu rodzajowi ponownego wykorzystania programy funkcyjne w Lua zazwyczaj składają się z dużej liczby bardzo małych elementów.
Rekurencja w Lua
Funkcje rekurencyjne wywołują same siebie, jak poniżej:
function factorial (n) if n == 0 then return 1 else
return n * factorial(n - 1) end end
print(factorial(5)) --> 120
Łatwo to zrozumieć, lecz wykonanie tej funkcji dla pewnych wartości n będzie powodować błąd przepełnienia stosu. Dzieje się tak, ponieważ przy każdym kolejnym uruchomieniu factorial(n - 1) maszyna musi zapamiętać na swoim stosie odwołanie do poprzedniej iteracji. Stos ma skończoną wielkość, więc ostatecznie kończy się miejsce i program wylatuje w powietrze:
print(factorial(-1))
lua: factorial.lua:5: stack overflow stack traceback: factorial.lua:5: in function 'factorial' factorial.lua:5: in function 'factorial'
Lua 5.0 dodała właściwe wywołania typu tail, rodzaj funkcyjnego goto, pozwalające na strukturę funkcji rekurencyjnej, w której nigdy nie kończy się miejsce na stosie:
function factorial (n)
return factorialUntil(n, 1) end
function factorialUntil (n, answer) if n == 0 then return answer else