Archiwa tagu: Haskell

10 migawek z kursu programowania funkcyjnego w Haskellu

Od dwóch miesięcy dość rzadko udaje mi się mieć wolne obie ręce. Przyczyna jest dobrze widoczna na załączonym obrazku 🙂 W grudniu udało mi się jednak ukończyć kurs  Introduction to Functional Programming w portalu edX.  Poniżej moje notatki i wspomnienia. Przykłady prezentowane w materiałach z kursu są napisane w Haskellu (i rzeczywiście tego języka chciałam się nauczyć), ale autorzy stawiają sobie za cel przedstawienie uniwersalnych zasad programowania funkcyjnego. Wiele zadań domowych można było wykonać w wybranym innym języku funkcyjnym, na przykład w Scali.

Czasem uda nam się trochę popracować!
Czasem uda nam się trochę popracować przy biurku z podnoszonym blatem!
1. Funkcje można definiować na kilka różnych sposobów.

Żeby nie było, że „programowanie funkcyjne” to tylko szumna nazwa: w Haskellu tę samą funkcję można zdefiniować na kilka sposobów, w zależności od potrzeb i osobistego stylu.

Poniżej kilka różnych definicji tej samej prostej funkcji. Dla zabawy zdefiniujmy funkcję przyjmującą jeden argument. Funkcja zwróci dodatnią wartość logiczną wtedy i tylko wtedy, gdy przekazany jej argument liczbowy będzie poprawną odpowiedzią na wielkie pytanie o życie, wszechświat i całą resztę.

Definicja z użyciem wyrażeń warunkowych:

Inne wyrażenia warunkowe (guarded expressions):

Dopasowanie do wzorca:

Najprostsza definicja świata, czyli zwykłe wyliczenie wartości:

Zachęcam do potestowania.

2. Lambdy to tylko funkcje bez nazwy.

Programowanie funkcyjne wtargnęło przemocą do wielu popularnych języków, więc nie spodziewam się zaszokować tłumów tą informacją. Ci jednak, którzy jeszcze nie wyszli poza świat obiektów, mogą odetchnąć z ulgą: lambdy to tylko funkcje bez nazwy. Przydatne w różnych okolicznościach, na przykład kiedy funkcja ma być użyta tylko raz i ma bardzo krótką definicję.

W Haskellu lambdy wyglądają tak (backslash, zwany też ukośnikiem wstecznym, ma w zamierzeniu przypominać grecki znak λ):

A tak, dla porównania, wyglądają w Javie:

Skąd taka nazwa? Inspiracją i matematyczną podstawą języków funkcyjnych jest rachunek lambda.

3. Praca z listami to sama przyjemność.

Na obronie pracy magisterskiej zadano mi pytanie, po czym na pierwszy rzut oka poznać kod napisany w języku Lisp. Właściwa odpowiedź jest dość prosta – po listach!

W Haskellu również listy przetwarza się bardzo przyjemnie. Można wygodnie dopasowywać je do wzorców, dzieląc listy na głowę i ogon, jak w tej poniższej definicji:

Użycie:

Jak widać w ostatnim przykładzie, wzorzec (head:tail) nie zostanie dopasowany do pustej listy.

Można także bardzo wygodnie generować listy, nawet nieskończone, przy użyciu dość intuicyjnej składni tzw. list comprehension. Oto przykład, trudniejszy w słownym opisie niż w implementacji: lista kwadratów tych liczb od 1 do 10, które są podzielne przez 2.

Ładne, prawda?

4. Brak stanu i inne wymagania.

Nie ma jednej obowiązującej definicji programowania funkcyjnego. Według wielu, jest to wręcz najgorzej zdefiniowany paradygmat programistyczny.

Jednym z najistotniejszych wyznaczników kodu funkcyjnego jest brak stanu – ta sama funkcja wywołana z tymi samymi argumentami powinna zawsze zwracać tę samą wartość, najlepiej bez efektów ubocznych (tj. zmieniania czegokolwiek w tle, np. wartości pól obiektu). W dużej mierze właśnie ta właściwość spowodowała renesans popularności języków funkcyjnych. Dlaczego jest to aż tak istotne?

  • Programując w ten sposób, niejako „w prezencie” dostajemy kod doskonale poddający się zrównoleglaniu. Osobne wątki nie muszą co chwilę zsynchronizować zmieniającego się, współdzielonego stanu.
  • Kod składający się z funkcji bez efektów ubocznych jest nieporównywalnie prostszy w zrozumieniu i utrzymaniu.
  • Wyniki raz wywołanych funkcji można zapamiętać w cache, by przy kolejnym wywołaniu z tymi samymi argumentami, po prostu sięgnąć do już wyznaczonej wartości.

Inne ważne cechy języków funkcyjnych to możliwość definiowania: funkcji wyższego rzędu (funkcji, które zwracają inne funkcje lub przyjmują inne funkcje jako argumenty) oraz, oczywiście, lambd.

5. Upierdliwa obsługa operacji I/O.

Czytanie z wiersza poleceń w Javie nigdy nie należało do przyjemności. Musimy pootwierać piętrowe strumienie, czytać z nich, sprawdzając, czy zostało coś jeszcze do pobrania, a na koniec pamiętać o ich zamknięciu (ok, w Javie 7 pojawił się interfejs AutoCloseable, wystarczy tylko pamiętać o jego użyciu).

Haskell pokazuje nam jednak, w pewnym sensie, że może być jeszcze gorzej. Dlaczego? Ponieważ odczytanie ciągu znaków z wejścia wymaga złamania nagięcia ograniczeń, które stanowią o sile języków funkcyjnych.

Funkcja, za pomocą której będziemy pobierali znaki z wejścia, siłą rzeczy musi zwracać odmienne wyniki dla tego samego argumentu. Co gorsza, musi też w tle zmieniać stan naszego świata (przy kolejnych wywołaniach znaki są konsumowane i inny znak czeka w kolejce na odczytanie).

Dla zachowania czystości kodu, a właściwie w celu oddzielenia kodu czystego od „skażonego”, operacje I/O w Haskellu są opakowywane w akcje. Całość „nieczystego” przetwarzania odbywa się wewnątrz akcji, z której na koniec możemy wyciągnąć czystą wartość za pomocą operatora <-.

Wygląda to stosunkowo niewinnie:

ale warto mieć świadomość, co dzieje się pod spodem. W rzeczywistości dotykamy tu najbardziej chyba przerażającego elementu języka, czyli monad. Kod powyżej czyta linię ze standardowego wejścia i wypisuje ją bez zmin na standardowe wyjście.

6. Brzydkie słowo na „m”.

Monady.

Tysiące tutoriali w Internecie. Długie wpisy (po polsku na przykład ten świetnie przygotowany i napisany tekst Jacka Laskowskiego), godzinne nagrania wideo. Duma lub poczucie wyższości osób, które wreszcie to zrozumiały, więc oficjalnie potrafią programować funkcyjnie.

Offtopic: słowo „monada” kojarzy mi się przede wszystkim ze zbiorem opowiadań „Zamknięty świat”, co dowodzi chyba, że nie była to lektura dla dziecka w podstawówce.

Wytłumaczenie, czym są monady, to temat na osobny, długi wpis – jak ten, który linkuję powyżej. W dużym uproszczeniu (zresztą moje rozumienie tego tematu to nadal właśnie duże uproszczenie): monady to kontenery, które pozwalają na tworzenie łańcuchów operacji, porównywanych do linii produkcyjnych, gdzie każde kolejne stanowisko dodaje coś od siebie. Za pomocą monad można symulować zmiany stanu i dodawać efekty uboczne. Monada obowiązkowo musi definiować dwie operacje, nazywane najczęściej return (włożenie wartości do kontenera) i bind (wyjęcie wartości w celu wstawienia jej do kolejnej funkcji zwracającej monadę; w Haskellu służy do tego operator >>=).

Podstawy teoretyczne monad to teoria kategorii. Po drodze trzeba jeszcze zrozumieć, czym są funktory i monoidy. Zainteresowanym polecam lekturę rozdziału poświęconego monadom w (dostępnej za darmo) książce Learn You a Haskell for Great Good!. Sama wrócę tam jeszcze wiele razy.

7. Istnieją frameworki webowe dla Haskella. Serio.

Można pisać aplikacje webowe w Haskellu. Istnieją gotowe frameworki. Więcej niż jeden! Osobiście znam kogoś, kto napisał w tym języku sklep internetowy.

8. Wyjście poza imperatywną strefę komfortu otwiera oczy raz na zawsze.

Kurs, przynajmniej na początku, wydał mi się bardzo prosty. Uznałam wstępnie, że Haskell ma zaskakująco niski próg wejścia – bo przecież nie programowałam wcześniej w żadnym języku funkcyjnym. Podczas rozmów z innymi kursantami dotarłam jednak do innej przyczyny: przez kilka lat sporo programowałam w Prologu. Wygląda na to, że kiedy raz podejmie się wysiłek przestawienia się na inny sposób myślenia – w tym wypadku nieimperatywny, nieobiektowy i mniej algorytmiczny – o wiele łatwiej wnika się w kolejne dziwactwa.

9. Programowanie funkcyjne, nie funkcjonalne!

Łatwo wpaść w pułapkę tłumaczenia angielskiej nazwy functional programming jako „programowanie funkcjonalne”. Nawet polska Wikipedia dopuszcza ten termin, a przecież jest on pozbawiony sensu! Functional znaczy „oparty o funkcje” (w sensie matematycznym), po polsku „funkcyjny”. „Funkcjonalny” to „związany z działaniem” lub „dobrze spełniający swoją rolę”. O programowaniu funkcyjnym można co prawda powiedzieć, że jest funkcjonalne… 😉 Jednak w pełni poprawna jest tylko pierwsza nazwa.

10. Można wziąć kasę za kurs online, w którym po wiedzę o najtrudniejszych zagadnieniach odsyła się do zewnętrznych stron

Kurs Haskella był pierwszym kursem online, za który zapłaciłam – i dobrze się stało, ponieważ bez takiej motywacji na pewno odpadłabym po którejś nieprzespanej nocy.

Tym boleśniej odczułam fuszerkę, na jaką pozwoliła sobie ekipa z Uniwersytetu Technicznego w Delft. Wiedza przekazana w ramach kursu nie była wystarczająca do odrobienia zadań domowych w jego drugiej części. Co gorsza, w wykładach pominięto właśnie te bardziej problematyczne zagadnienia, począwszy od tego „czym, u licha, jest ta monada?”.

Opisy niektórych zadań domowych rozpoczynały się niepozornym poleceniem przeczytania treści zawartych na zewnętrznej stronie, a zewnętrzne strony wyglądały jak, nie przymierzając, strony man w Linuksie. Suchar.

Wieża Babel. Skąd tyle języków programowania? (część 1 z 3)

Tekst jest oparty na mojej prezentacji z konferencji Kariera IT

Świat staje się coraz mniejszy. Wszyscy uczymy się angielskiego, a lokalne dialekty odchodzą w zapomnienie. Dlaczego wciąż upieramy się, by z komputerem rozmawiać na tyle różnych sposobów? Ile języków powinien znać programista? Od którego zacząć?

Postaram się krótko opowiedzieć o współczesnych językach programowania i różnicach pomiędzy nimi. Dla porządku zacznę od (króciutkiego!) rysu historycznego. Dalej będzie z górki! 🙂

Pierwszy programista (nie róbmy z tego tajemnicy: chodził w spódnicy)

Jako pierwszego na świecie programistę często wymienia się Adę Lovelace. Ada była córką poety, Lorda Byrona. W latach 1842-1843 (!) przetłumaczyła z francuskiego rozprawę poświęconą „maszynie analitycznej” Charlesa Babbage’a. Tłumaczenie opatrzyła notatkami, w których znalazł się pierwszy na świecie opis algorytmu przeznaczonego do wykonania przez maszynę (algorytm wyznaczał kolejne liczby Bernoulliego). Działający egzemplarz takiej maszyny udało się zbudować dopiero w 1991 roku. Dokonało tego Muzeum Nauki w Londynie, przy użyciu materiałów dostępnych w czasach Babbage’a.

Ada Lovelace: portret

Wyrazami uznania dla pracy Ady Lovelace są między innymi język programowania Ada stworzony na początku lat osiemdziesiątych oraz nagroda Ada Award przyznawana „wybitnym dziewczętom i kobietom w sektorach cyfrowych”.

Odrobina kontekstu: kamienie milowe historii programowania

To nie jest wpis o historii programowania, dlatego wypunktuję tylko kilka przełomowych wydarzeń:

  • 1936: Alan Turing opisuje Maszynę Turinga, hipotetyczny „komputer” złożony z głowicy oraz nieskończonej taśmy, który może wykonywać algorytmy i stanowi teoretyczny model obliczeń.
  • 1943-1945: Powstaje ENIAC, powszechnie uważany za pierwszy komputer.
  • 1940-1960: Dominują języki niskopoziomowe. Na ich tle wWyróżnia się FORTRAN, język wyższego poziomu z własnym kompilatorem.
  • 1960-1970: Powstają stosowane do dzisiaj języki i paradygmaty: język C, Smalltalk (obiektowy), Prolog.
  • 1990-…: Internet staje się ogólnodostępny. Dominuje paradygmat obiektowy. Pojawia się sporo języków funkcyjnych i skryptowych.
  • 2010-…: Zwrot w stronę programowania funkcyjnego, metaprogramowanie i mechanizm refleksji, nacisk na wielowątkowość.

Z głową w chmurach: języki niskiego i wysokiego poziomu

Jeśli – chcąc nauczyć się nowego języka programowania – zaczniesz szukać informacji na jego temat w Internecie, prawie na pewno natkniesz się na informację o tym, że „X to język programowania wysokiego poziomu, który …”. Jeden za drugim, którego byś nie sprawdził, wszystkie są wysokopoziomowe. Co to znaczy?

Spójrz na fragmenty kodu w poniższej tabelce.

Kod maszynowy Asembler C

 

 

Każdy z przedstawionych fragmentów kodu (wzięłam je z Wikipedii; wystarczył mi jeden semestr z Asemblerem żeby wiedzieć, że nigdy więcej nie chcę go dotykać) robi to samo: wyznacza n-ty wyraz ciągu Fibonacciego.

Po lewej mamy kod maszynowy – kolejne liczby (zapisane szesnastkowo) to rozkazy procesora i ich argumenty. Kod taki jest nieprzenośny, tj. zależny od architektury. Przykładowy kod jest przeznaczony dla 32-bitowej maszyny x86. Programowanie w tym języku wymaga zapamiętania numerów poszczególnych instrukcji, lub korzystania z rozbudowanej ściągi (słownika). Jest to kod absolutnie niskopoziomowy.

Środkowa kolumna to Asembler (ang. Assembly language). To również język niskopoziomowy. Wygląda inaczej niż kod maszynowy, jednak w rzeczywistości stanowi odwzorowanie „jeden do jeden” instrukcji procesora, tyle że w sposób łatwiejszy do zrozumienia i zapamiętania przez człowieka. W przykładowym kodzie widać odwołania do rejestrów procesora x86 oraz do stosu.

Po prawej stronie język C. Kod różni się od przykładów po lewej stronie brakiem odwołań do architektury maszyny, na której działa. Wartości będą w końcu pobierane ze stosu, ale nigdzie nie oznaczamy, w jaki sposób ma się to odbyć. Funkcja ma zwrócić wartość, ale nigdzie w kodzie nie określamy mechanizmu jej przekazania. Wprowadzamy własne abstrakcje – zmienne lokalne wewnątrz instrukcji warunkowych oraz pętli – i nie przejmujemy się sposobem ich obsługi na docelowej maszynie.

Na podstawie powyższego możesz logicznie wywnioskować, że C jest językiem wysokiego poziomu. Jednak gdy podzielisz się tą opinią z zaprzyjaźnionymi programistami, większość z nich z niedowierzaniem pokręci głową. Dzisiaj C jest uznawany za, w najlepszym razie, język „średniego poziomu”. Wprowadza sporo użytecznych abstrakcji, ale jednocześnie (czego w naszym przykładzie akurat nie widać) pozwala na bezpośrednie odwołania do pamięci – nie do pomyślenia w większości języków wysokopoziomowych.

Możesz jeszcze natknąć się na określenie super high level language, czyli język bardzo wysokiego poziomu. Tym terminem określa się najczęściej języki dziedzinowe, zwiększające produktywność programisty poruszającego się na co dzień po specyficznym, zamkniętym obszarze. Z reguły są tą jednak tylko (?) nakładki na istniejące języki programowania wysokiego poziomu.

Rozkazuję ci… Języki imperatywne i deklaratywne

Kolejny ważny podział wśród języków programowania to rozróżnienie na paradygmat imperatywny i deklaratywny.

  • Program imperatywny zawiera ciąg instrukcji, zmieniających stan programu. Wydajemy komputerowi „rozkazy”: zwiększ wartość x o 5, zwróć wynik. Przykłady języków imperatywnych to Perl, Python, Java oraz wszystkie języki przedstawione w tabelce w poprzednim podrozdziale.
  • Program deklaratywny nie mówi komputerowi, jakie kroki ten ma wykonać. Zamiast tego, programista opisuje warunki, jakie musi spełniać rozwiązanie. Ważne i pożądane cechy języka deklaratywnego to bezstanowość i determinizm. Te same dane wejściowe zawsze prowadzą do uzyskania tego samego wyniku, nie wpływa na nie żaden wewnętrzny „stan”. Przykłady języków deklaratywnych to Haskell, Erlang i Prolog.

Świat imperatywny: klasy i prototypy

Jeśli chodzi o programowanie imperatywne, od dawna już (co najmniej od początku obecnego wieku) dominuje programowanie obiektowe, które wyparło mniej ustrukturyzowane programowanie proceduralne. W ścisłym ujęciu program obiektowy to program, którego wykonanie sprowadza się do przesyłania komunikatów pomiędzy obiektami. Nie wszyscy jednak wiedzą, że programować obiektowo można w dwóch „smakach”: przy użyciu klas lub paradygmatów.

Sytuację dobrze ilustruje ten oto obrazek (mojego autorstwa, wreszcie mogę się popisać!), inspirowany slajdem z prezentacji na temat zaskakujących elementów języka JavaScript:

Prototypy kontra klasy, ujęcie biologiczne i gratka dla fanów mojej kreski (są tacy!).
Prototypy kontra klasy, ujęcie biologiczne, Gratka dla fanów mojej kreski (są tacy!)

W podejściu klasycznym (nazwa jest tym bardziej odpowiednia, że to podejście dominuje – np. Java, C++) opisujemy świat za pomocą klas. W praktyce klasą może być „Okno Przeglądarki” albo „Ramka”, jednak w zastosowaniu edukacyjnym lepszym przykładem będzie nieśmiertelna klasa „Zwierzę”. Klasa Zwierzę ma pewne atrybuty: liczbę nóg lub umiejętność wydawania pewnego dźwięku. Klasa ta może mieć podklasę, na przykład Słoń. Słoń ma wszystkie atrybuty klasy Zwierzę, ale może definiować własne – na przykład trąba i jej długość 😉

W oparciu o klasy tworzymy obiekty, czyli konkretne instancje (egzemplarze) poszczególnych klas. I tak DumboMamaDumbo to dwa różne obiekty tej samej klasy Słoń. Klasy możemy podzielić na konkretne (które mogą mieć instancje) i abstrakcyjne (bardziej ogólne, pełniące tylko rolę szablonów).

Alternatywą jest podejście prototypowe, dostępne na przykład we wspomnianym już tutaj języku JavaScript. Jeśli programujemy z prototypami, całkowicie wyzbywamy się klas. Wszystko jest obiektem. Zwierzę to obiekt. Słoń to obiekt, dla którego obiekt Zwierzę jest prototypem. Dumbo to kolejny obiekt, wzorowany na Słoniu. Koneserzy tej wersji przekonują, że dopiero przy takim podejściu, gdzie wszystko jest obiektem, możemy mówić o programach prawdziwie obiektowych.

Jaka jest praktyczna różnica? Prototypy, jako obiekty, można modyfikować w czasie wykonania. Klasy są usztywnione – jeśli powiemy, że słoń ma cztery nogi, nie będziemy gotowi do obsłużenia egzemplarza z pięcioma. Ma to jednak swoją cenę. Większość języków prototypowych jest typowana dynamicznie. Oznacza to, że typ obiektu jest określany dopiero w czasie wykonania. Dla takich języków o wiele trudniej jest tworzyć dobre IDE (zintegrowane środowisko programistyczne). W przypadku statycznie definiowanych klas, edytor może nam sporo pomóc: np. podpowiedzieć, jakie funkcje da się wywołać na danym obiekcie.

Ograniczenia wprowadzane przez stosowanie klas stają się jednak coraz mniej uciążliwe z powodu rozbudowanych mechanizmów refleksji (zmiany istniejącego kodu w czasie wykonania) w nowoczesnych językach programowania.

Świat deklaratywny: języki funkcyjne i programowanie w logice

Języki funkcyjne przeżywają obecnie prawdziwy boom popularności. Główną przyczyną tego stanu (sic!) rzeczy jest ich bezstanowość i brak skutków ubocznych – cechy te ułatwiają programowanie współbieżne. Jest to istotne, ponieważ w roku 2014 dobiegamy do kresu prawa Moore’a, wieszczące (w uproszczeniu) wykładniczy wzrost szybkości układów scalonych. Zamiast coraz szybszych procesorów mamy teraz procesory z większą liczbą rdzeni i coś z tym fantem trzeba zrobić.

Program napisany w języku funkcyjnym sprowadza się do wartościowania (ewaluacji) funkcji matematycznych. Unikamy przy tym przechowywania i modyfikowania stanu. Do dyspozycji mamy języki czysto funkcyjne, jak Haskell (z dość dużym progiem wejścia, uwielbiane przez snobistycznych doświadczonych developerów), oraz nieco bardziej przystępne (acz kojarzące się z potworem Frankensteina) języki mieszane, jak Scala, które pozwalają na łamanie części zasad, przy jednoczesnej praktycznej dostępności pewnych silnych stron tego paradygmatu (jak wygodne wykonywanie funkcji na wszystkich elementach kolekcji).

Programowanie funkcyjne to duży temat, któremu zamierzam w przyszłości poświęcić osobny wpis.

Inny rodzaj programowania deklaratywnego to programowanie w logice, szczególnie przydatne przy problemach zahaczających o zagadnienia sztucznej inteligencji (rozumienie języka naturalnego, rozwiązywanie łamigłówek). W tym podejściu, zamiast szukać algorytmów,, musimy zdefiniować zależności i ograniczenia (ang. constraints) obowiązujące w danej dziedzinie. Interpreter języka logicznego, takiego jak Prolog, podejmie następnie próbę rozwiązania problemu „za nas” w oparciu np. o rachunek predykatów pierwszego rzędu.

Pierwsze doświadczenia z programowaniem w logice bywają bolesne. O ile w większości języków możemy napisać coś w stylu

X = 1; X = X + 1;

i spodziewać się odpowiedzi 2, o tyle Prolog w podobnych okolicznościach może zaskoczyć nas krótką i konstruktywną odpowiedzią NIE (X nigdy nie będzie równe X+1). Po co w ogóle pchać się w te cudaczne rejony?

Ano chociażby dlatego, że w Prolog pozwala na rozwiązanie bardzo skomplikowanych problemów (kostka Rubika, Sudoku) w zaskakująco niewielkiej liczbie linii kodu.

Poniżej kompletny kod rozwiązujący Sudoku o rozmiarach 9×9, napisany w Prologu. Program pochodzi z bloga Programmable Life, tam też znajdziesz kompletne objaśnienie.

W następnych częściach

PS. Zdjęcie w nagłówku

Zdjęcie przedstawia kartę perforowaną z zapisem kodu w języku Fortran. Kart perforowanych po raz pierwszy użyto w roku 1725 do sterowania pracą krosna.

W 1889 roku Herman Hollerith opatentował nowoczesną postać karty (oraz taśmy) dziurkowanej do zapisu danych. Zainspirował go system stosowany przez konduktorów amerykańskich kolei, którzy za pomocą dziurek kodowali na biletach wygląd pasażera („wysoki, niebieskie oczy”), który się danym biletem posługuje (żeby pasażerowie nie wymieniali się biletami).

Karty perforowane były szeroko stosowane do zapisu danych i programów aż do lat osiemdziesiątych XX wieku. Ich ostateczny upadek miał miejsce w roku 2000, gdy przez resztki papieru pozostałe w otworach kart nie wszystkie głosy zostały policzone poprawnie i w efekcie trzeba je było zliczyć ręcznie.