Programowanie funkcyjne/Wstęp: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Przemek (dyskusja | edycje)
m Zastępowanie tekstu – „<math> ” na „<math>”
 
(Nie pokazano 46 wersji utworzonych przez 4 użytkowników)
Linia 1: Linia 1:
==Wstęp==
Celem tego przedmiotu jest przedstawienie programowania funkcyjnego – paradygmatu i stylu programowania oraz towarzyszących mu technik programistycznych.
W trakcie zajęć będziemy korzystać z języka progamowania
[http://caml.inria.fr/ocaml Ocaml].
(Nie jest to jednak w żadnej mierze kurs programowania w Ocamlu.)
Zaczniemy od krótkiego intuicyjnego porównania paradygmatu programowania funkcyjnego oraz najpopularniejszego paradygmatu programowania – programowania imperatywnego.
W kilku początkowych wykładach przedstawiamy podstawy programowania w Ocamlu.
Następnie poznamy jedną z podstawowych koncepcji programowania funkcyjnego: procedury wyższych rzędów.
W kolejnym wykładzie zaprezentujemy (uproszczoną) semantykę operacyjną podstawowego fragmentu języka.
Następnie zajmiemy się modułami (strukturami), ich interfejsami (sygnaturami) oraz modułami sparametryzowanymi (funktorami).
Dowiemy się też, jak w języku funkcyjnym mogą być zrealizowane konstrukcje imperatywne.
Zostaną one następnie zastosowane do zaimplementowania spamiętywania i uleniwiania obliczeń.
Te zaś techniki zostaną użyte do zaimplementowania strumieni.
Na zakończenie przedstawimy krótki przegląd innych najpopularniejszych funkcyjnych języków programowania: Haskella i Scheme'a (dialektu Lispu).
== Porównanie paradygmatu funkcyjnego i imperatywnego ==
== Porównanie paradygmatu funkcyjnego i imperatywnego ==
<p align="justify">
<p align="justify">
Jaki jest cel programowania?
Najbardziej popularny paradygmat programowania to programowanie imperatywne.  
Pisząc program staramy się osiągnąć jakiś cel.
Języki programowania takie, jak C, C++, Java, czy Pascal, to języki imperatywne.  
Jak w każdej działalności, dobrze jest najpierw uzmysłowić sobie
(Dodatkowo, część z nich to języki obiektowe.)
co chcemy osiągnąć, a dopiero potem starać się to osiągnąć ---
Istotą programowania imperatywnego jest to, że program zawiera zmienne (lub obiekty) oraz operacje.
inaczej możemy uzyskać niezamierzone rezultaty.
Zmienne i obiekty charakteryzują się stanem, który może ulegać zmianie w czasie.  
Cel, do którego dążymy zwykle opisuje się w tzw. specyfikacji.
Operacje służą wykonywaniu obliczeń, zmianie stanu zmiennych (przypisanie) lub obiektów oraz kontrolują
Specyfikacja może być mniej lub bardziej formalna.
przepływ sterowania w obrębie programu.  
Jeśli jest sformalizowana, to nasz cel ma zwykle postać
relacji między danymi, a wynikami.
Program, który tworzymy powinien mieścić się w ramach określonych
przez specyfikację.
Weryfikacja poprawności programu polega na sprawdzeniu, że faktycznie
tak jest.
</p>
</p>
<p align="justify">
<p align="justify">
Paradygmat funkcyjny polega na tym, że pisząc program tworzymy
Paradygmat funkcyjny polega na tym, że pisząc program, tworzymy coraz bardziej skomplikowane pojęcia matematyczne, aż do osiągnięcia celu.  
pojęcia matematyczne, coraz bardziej skomplikowane, aż do
Pojęcia te mają postać stałych i funkcji, stąd nazwa ''programowanie funkcyjne''.  
osiągnięcia celu.
Oczywiście tworzone funkcje są wykonywalne, tzn. dostarczając argumentów możemy obliczyć ich wartości.  
Pojęcia te mają postać stałych i funkcji, stąd nazwa
''programowanie funkcyjne''.Oczywiście tworzone funkcje są wykonywalne, tzn. dostarczając
argumentów możemy obliczyć ich wartości.
Weryfikacja programu funkcyjnego polega na udowodnieniu, że
tworzone przez nas funkcje zawierają się w relacjach określonych
przez specyfikacje (i są określone na odpowiednich dziedzinach).
</p>
</p>
<p align="justify">
<p align="justify">
Charakterystyczne dla programowania funkcyjnego jest również to,
Charakterystyczne dla programowania funkcyjnego jest również to, że funkcje są "obywatelami pierwszej kategorii", tzn. przysługują im wszystkie te prawa, co innego rodzaju wartościom.  
że funkcje są "obywatelami pierwszej kategorii", tzn.
W tej chwili nie jest jeszcze jasne o jakie prawa może chodzić.  
przysługują im wszystkie te prawa, co innym wartościom.
Można jednak śmiało powiedzieć, że jest to jedna z fundamentalnych zasad programowania funkcyjnego.
W tej chwili nie jest jeszcze jasne o jakie prawa może chodzić.
Jak zobaczymy, zasada ta jest jak konstytucja i wiele innych cech programowania funkcyjnego będzie z niej wynikać.  
Można jednak śmiało powiedzieć, że jest to jedna z fundamentalnych
zasad programowania funkcyjnego.
</p>
</p>
<p align="justify">
<p align="justify">
Porównajmy paradygmat funkcyjny i imperatywny.
Porównajmy zastosowanie paradygmatów funkcyjnego i imperatywnego do tworzenia programów.  
Programowanie imperatywne jest powszechnie znane.
Programowanie imperatywne jest powszechnie znane.  
Na czym jednak polega paradygmat imperatywny?
Na czym jednak polega paradygmat imperatywny?  
W naszych programach zwykle staramy się wymodelować istotny dla nas
W naszych programach zwykle staramy się wymodelować istotny dla nas fragment otaczającego nas świata.  
fragment otaczającego nas świata.
Świat ten składa się (na codzienny użytek) z przedmiotów.  
Świat ten, składa się (na codzienny użytek) z przedmiotów.
Przedmiotom tym odpowiadają w programach imperatywnych zmienne lub obiekty.  
Przedmiotom tym odpowiadają w programach imperatywnych
Przedmioty w świecie rzeczywistym zmieniają się wraz z upływem czasu.  
zmienne lub obiekty.
Podobnie więc zmienne i obiekty obdarzone są stanem, który może
Przedmioty w świecie rzeczywistym zmieniają się wraz z upływem czasu.
się zmieniać w czasie. Dane są to pewne przedmioty, na których należy wykonywać określone czynności, w wyniku których przeistoczą się one w wyniki.  
Podobnie więc, zmienne i obiekty obdarzone są stanem, który może
Upływ czasu w świecie rzeczywistym jest modelowany przez upływ czasu w komputerze, a zmiany stanów są realizowane przez przypisania.  
się zmieniać w czasie.
Dane są to pewne przedmioty, na których należy wykonywać określone
czynności, w wyniku których przeistoczą się one w wyniki.
Upływ czasu w świecie rzeczywistym jest modelowany przez upływ czasu
w komputerze, a zmiany stanów są realizowane przez przypisania.  
</p>
</p>
<p align="justify">
<p align="justify">
Pojęcie czasu nie jest tak bardzo obecne w programach funkcyjnych.
Pojęcie czasu nie jest tak bardzo obecne w programach funkcyjnych.  
Oczywiście, aby użyć pojęcia musimy je najpierw zdefiniować, a
Oczywiście, aby użyć jakiegoś pojęcia musimy je '''najpierw''' zdefiniować, a żeby uzyskać wynik funkcji,
żeby uzyskać wynik funkcji musimy najpierw dostarczyć argumentów.
musimy '''najpierw''' dostarczyć argumentów.  
W "czystym" programowaniu funkcyjnym nie występuje jednak
W "czystym" programowaniu funkcyjnym nie występuje jednak pojęcie zmiennej ani przypisania.  
pojęcie zmiennej, ani przypisania.
Nie ma też pętli jako konstrukcji związanych z czasem i zmianą.
Nie ma też pętli, jako konstrukcji związanych z czasem i zmianą.
Tworzenie programu polega na tworzeniu coraz to bardziej złożonych funkcji i stałych oraz na obliczaniu wartości wyrażeń.  
</p>
</p>


== Charakterystyka programowania funkcyjnego ==
== Charakterystyka programowania funkcyjnego ==
<p align="justify">
<p align="justify">
Jeśli nie ma zmiennych, przypisania, ani pętli, to co jest?
Jeśli nie ma zmiennych, przypisania ani pętli, to co jest?
Zamiast operacji mamy obliczanie wyrażeń.
Zamiast operacji mamy obliczanie wyrażeń.
Zamiast przypisań mamy definiowanie stałych i funkcji
Zamiast przypisań mamy definiowanie stałych i funkcji
(funkcje to w pewnym sensie też stałe, tylko bardziej skomplikowane).
(funkcje to w pewnym sensie też stałe, tylko o bardziej skomplikowanej naturze).
Zamiast pętli mamy rekurencję.
Zamiast pętli mamy rekurencję.
</p>
</p>
Linia 77: Linia 73:
Mimo wszystko, łatwiej jest weryfikować programy funkcyjne niż
Mimo wszystko, łatwiej jest weryfikować programy funkcyjne niż
imperatywne.
imperatywne.
</p>
<p align="justify">
Specyfikacja programu może być mniej lub bardziej formalna.
Jeśli jest sformalizowana, to zwykle ma postać relacji między danymi a wynikami.
Program, który tworzymy, powinien mieścić się w ramach określonych przez specyfikację.
Weryfikacja poprawności programu polega na sprawdzeniu, że faktycznie tak jest.
W przypadku programów funkcyjnych zarówno specyfikacja jak i program mają postać
obiektów matematycznych.
Stąd łatwiej wykazać poprawność programu.
</p>
<p align="justify">
W przypadku programów imperatywnych, musimy wnioskować o zachodzących
W przypadku programów imperatywnych, musimy wnioskować o zachodzących
zmianach.
zmianach stanów zmiennych lub obiektów.
Jeżeli mamy do czynienia z aliasingiem, musimy wiedzieć, co ulega
Jeżeli mamy do czynienia z aliasingiem lub strukturami wskaźnikowymi,  
zmianie na skutek każdego przypisania, a co nie.
musimy wiedzieć, co ulega zmianie na skutek każdego przypisania, a co nie.
Narażeni jesteśmy na błędy typu: czemu ta zmienna zmieniła wartość?
Narażeni jesteśmy na błędy typu: czemu ta zmienna zmieniła wartość?
czy te dwie instrukcje powinny być wykonywane w tej, czy odwrotnej
czy te dwie instrukcje powinny być wykonywane w tej, czy odwrotnej
kolejności?
kolejności?
W przypadku programowania funkcyjnego nie mamy takich problemów,
W przypadku programowania funkcyjnego takie problemy w ogóle nie powstają,  
co poprawia czytelność programów i ułatwia ich weryfikację.
co poprawia czytelność programów i ułatwia ich weryfikację.
</p><p align="justify">
</p>
<p align="justify">
Programowanie funkcyjne daje nam również do ręki pewne techniki
Programowanie funkcyjne daje nam również do ręki pewne techniki
programistyczne, które nie występują w  
programistyczne, które nie występują w  
Linia 92: Linia 100:
rzędów, strumienie, czy funktory.
rzędów, strumienie, czy funktory.
Poznamy je w trakcie tego wykładu.
Poznamy je w trakcie tego wykładu.
</p><p align="justify">
</p>
<p align="justify">
Skłamałbym, gdybym twierdził, że programowanie funkcyjne jest
Skłamałbym, gdybym twierdził, że programowanie funkcyjne jest
pozbawione wad.
pozbawione wad.
Jedna z takich wad, którą zwykle programiści odczuwają boleśnie
Jedna z takich wad, którą zwykle programiści odczuwają boleśnie,
jest brak (w czystym programowaniu funkcyjnym) imperatywnych tablic.
jest brak (w czystym programowaniu funkcyjnym) imperatywnych tablic.
Można się jednak bez nich obyć, czasami modyfikując algorytm, a
Można się jednak bez nich obyć, czasami modyfikując algorytm, a
w najgorszym przypadku zwiększając złożoność czasową o czynnik
w najgorszym przypadku zastępując je słownikowymi strukturami danych
<math>O(\log n)</math> --- używając słownikowych struktur danych zwanych
(zwanymi mapami), co zwiększa złożoność czasową o czynnik
mapami.
<math>O(\log~n)</math>.
</p>
<p align="justify">
Programowanie imperatywne można porównać do klucza francuskiego, a programowanie funkcyjne do kombinerek.
Są to dwa narzędzia o podobnym, ale nie identycznym, przeznaczeniu.
Dobry programista powinien znać oba narzędzia i wiedzieć, kiedy które zastosować.
Czasami należy użyć jednego z tych podejść, czasami drugiego, a czasami można
je łączyć, tak aby tworzyć programy łatwo i tworzyć programy łatwe do zrozumienia.
</p>
</p>


== Ocaml ==
== Ocaml ==
Na zajęciach będziemy korzystać z języka Ocaml (Objective Caml).
<p align="justify">
Jest on dostępny pod adresem http://caml.inria.fr/.
W trakcie tego wykładu będziemy przede wszystkim korzystać z języka [http://en.wikipedia.org/wiki/OCaml Ocaml] (Objective Caml).
Jest on dostępny pod adresem [http://caml.inria.fr/ http://caml.inria.fr].
Ocaml to język:
Ocaml to język:
</p>
* w pełni funkcyjny,   
* w pełni funkcyjny,   
* inkrementacyjny (cykl: wczytaj, oblicz/zdefiniuj, wypisz),
* z kontrolą typów,
* z polimorfizmem,  
* z polimorfizmem,  
* ze statycznym wiązaniem identyfikatorów,
* ze (statyczną) kontrolą typów,
* pozwalający na modularyzację programów,  
* pozwalający na modularyzację programów,  
* bogaty --- rozbudowany system typów, biblioteki (481 str.&nbsp;dokumentacji, tfu!),
* zawierający konstrukcje imperatywne i wyjątki (tzn. konstrukcji imperatywnych można używać tylko w drodze wyjątku ;-),  
* z konstrukcjami imperatywnymi (hurra?) i wyjątkami (tzn.&nbsp;imperatywności można używać tylko w drodze wyjątku ;-), z których będziemy nawet korzystać (a jednak!),
* obiektowy (z tego akurat nie będziemy korzystać),
* obiektowy (z tego akurat nie będziemy korzystać),
* ze statycznym wiązaniem identyfikatorów.
 
== Haskell ==
<p align="justify">
Innym popularnym językiem funkcyjnym jest [http://en.wikipedia.org/wiki/Haskell_(programming_language) Haskell] ([http://www.haskell.org/haskellwiki/Haskell]).
Haskell to język:
</p>
* czysto funkcyjny (bez konstrukcji imperatywnych), 
* leniwy,
* z polimorfizmem,
* ze statycznym wiązaniem identyfikatorów,
* ze (statyczną) kontrolą typów,
* pozwalający na modularyzację programów,
 
== Scheme ==
<p align="justify">
[http://en.wikipedia.org/wiki/Scheme_(programming_language)] to dialekt języka [http://en.wikipedia.org/wiki/Lisp_programming_language Lisp] opracowany na MIT.
Jest to język:
</p>
* w pełni funkcyjny,
* minimalistyczny, 
* z polimorfizmem,
* z dynamicznym wiązaniem identyfikatorów,
* z dynamiczną kontrolą typów,
* zawierający konstrukcje imperatywne,


== Kilka poglądowych przykładów ==
== Kilka poglądowych przykładów ==
<p align="justify">
Przyjrzyjmy się kilku '''bardzo prostym''' przykładom.
Przyjrzyjmy się kilku '''bardzo prostym''' przykładom.
Skoro mówimy o programowaniu funkcyjnym, spróbujmy zaprogramować
Skoro mówimy o programowaniu funkcyjnym, spróbujmy zaprogramować
kilka prostych funkcji.
kilka prostych funkcji.
</p>
 
{{
{{uwaga||uwaga_1|Od tej pory będziemy używać słowa ''funkcja'' na określenie pojęcia matematycznego, a słowa ''procedura'' na określenie pojęcia programistycznego}}
uwaga||uwaga_1|Od tej pory będziemy używać słowa ''funkcja'' na określenie pojęcia matematycznego, a słowa ''procedura'' na określenie pojęcia programistycznego
}}




=== Wartość bezwzględna ===
=== Wartość bezwzględna ===
Wzór:
Funkcję wyznaczającą wartość bezwzględną możemy opisać wzorem:


<center><math>|x| = \begin{cases}x & \textrm{;\ gdy } x \ge 0 \\-x & \textrm{;\ gdy } x < 0\end{cases}</math></center>
<center><math>|x| =  
\begin{cases}
x & \text{; gdy } x \ge 0 \\
-x & \text{; gdy } x < 0
\end{cases}
</math></center>


Pascal:
W Pascalu możemy ją zaimplementować w następujący sposób:


  '''function''' abs(x : real) : real;
  '''function''' abs(x : real) : real;
Linia 140: Linia 182:
  '''end''';
  '''end''';


Ocalm:
Natomiast w Ocamlu jej definicja będzie miała taką postać:


  '''let''' abs x =  
  '''let''' abs x =  
  &nbsp;&nbsp;'''if''' x >= 0.0 '''then''' x '''else''' -. x;;
  &nbsp;&nbsp;'''if''' x >= 0.0 '''then''' x '''else''' -. x;;


Zwróćmy uwagę na funkcyjny \codeline{if}.
W Haskellu taką:
 
abs x | x > 0 = x
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;| x < 0 = -x
 
A w Scheme'ie taką:
 
('''define''' (abs x)
&nbsp;&nbsp;('''if''' (>= x 0) x (- x)))
 
 
Zwróćmy uwagę na funkcyjny <tt>if</tt>.
Nie obejmuje on '''instrukcji''', ale '''wyrażenia'''.
Powyższy kod w językach funkcyjnych jest bliższy podanemu wzorowi niż programowi w Pascalu.


=== Silnia ===
=== Silnia ===


Wzór:
Wzór opisujący silnię ma postać:


<center><math>\begin{matrix} 0! = 1 \\ n! = n \cdot (n-1)! \end{matrix}</math></center>
<center><math>\begin{matrix} 0! = 1 \\ (n+1)! = (n+1) \cdot n! \end{matrix}</math></center>


Pascal:
Oto iteracyjny program w Pascalu obliczający silnię:


  '''function''' silnia (n : integer): integer;
  '''function''' silnia (n : integer): integer;
Linia 165: Linia 220:
  '''end''';
  '''end''';


Ocaml:
W językach funkcyjnych iteracja jest wyrażona za pomocą rekurencji.
W Ocamlu:


  '''let''' '''rec''' silnia n =  
  '''let''' '''rec''' silnia n =  
  &nbsp;&nbsp;'''if''' n < 2 '''then''' 1 '''else''' n * silnia (n - 1);;
  &nbsp;&nbsp;'''if''' n < 2 '''then''' 1 '''else''' n * silnia (n - 1);;
W Haskellu:
silnia 0 = 1
silnia n = n * silnia (n - 1)
W Scheme'ie:
('''define''' (factorial n)
&nbsp;&nbsp;('''if''' (= n 0) 1 (* n (factorial (- n 1)))))
Znowu, program funkcyjny bardziej przypomina wzór niż program w Pascalu.


=== Liczby Fibonacciego ===
=== Liczby Fibonacciego ===
Linia 176: Linia 244:
<center><math>\begin{matrix} \mbox{Fib}_0 = 0 \\ \mbox{Fib}_1 = 1 \\ \mbox{Fib}_n = \mbox{Fib}_{n-2} + \mbox{Fib}_{n-1} \end{matrix}</math></center>
<center><math>\begin{matrix} \mbox{Fib}_0 = 0 \\ \mbox{Fib}_1 = 1 \\ \mbox{Fib}_n = \mbox{Fib}_{n-2} + \mbox{Fib}_{n-1} \end{matrix}</math></center>


Pascal:
Program w Pascalu:


  '''function''' fib (n : integer) : integer;
  '''function''' fib (n : integer) : integer;
Linia 192: Linia 260:
  '''end''';
  '''end''';


Ocaml:
Program w Ocamlu:


  '''let rec''' fib n =  
  '''let rec''' fib n =  
  &nbsp;&nbsp;'''if''' n < 2 '''then''' n '''else''' fib (n - 1) + fib (n - 2);;
  &nbsp;&nbsp;'''if''' n < 2 '''then''' n '''else''' fib (n - 1) + fib (n - 2);;
W Haskellu:
fib n = '''if''' n < 2 '''then''' n '''else''' fib (n - 1) + fib (n - 2)
W Scheme'ie:
('''define''' (fib n)
&nbsp;&nbsp;('''if''' (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))


<p align="justify">
<p align="justify">
Taki program jest prostym przełożeniem wzoru na program, jest jednak
Taki program jest prostym przełożeniem wzoru na język programowania.
nieefektywny. Możemy zapisać program działający podobnie do powyższego programu
Jest on jednak nieefektywny.  
Możemy zapisać program działający podobnie do powyższego programu
w Pascalu, gdzie w każdym kroku iteracji pamiętamy dwie kolejne
w Pascalu, gdzie w każdym kroku iteracji pamiętamy dwie kolejne
liczby Fibonacciego.
liczby Fibonacciego.
Iteracja jest tu wyrażona za pomocą rekurencji.
</p>
</p>
W Ocamlu:


  '''let''' '''rec''' fibpom n =  
  '''let''' '''rec''' fibpom n =  
Linia 210: Linia 291:
  &nbsp;&nbsp;&nbsp;&nbsp;'''let''' (a, b) = fibpom (n - 1)
  &nbsp;&nbsp;&nbsp;&nbsp;'''let''' (a, b) = fibpom (n - 1)
  &nbsp;&nbsp;&nbsp;&nbsp;'''in''' (b, a + b);;
  &nbsp;&nbsp;&nbsp;&nbsp;'''in''' (b, a + b);;
Procedura pomocnicza <pre>fibpom</pre> spełnia specyfikację: <math>\mbox{fibpom n} = (\mbox{Fib}_n, \mbox{Fib}_{n+1})</math>.


  '''let''' fib n =
  '''let''' fib n =
Linia 217: Linia 296:
  &nbsp;&nbsp;'''in''' a;;
  &nbsp;&nbsp;'''in''' a;;


W trakcie wykładu poznamy lepsze sposoby definiowania liczb Fibonacciego.
W Haskellu:
 
fibpom n =
&nbsp;&nbsp;'''if''' n == 0 '''then'''
&nbsp;&nbsp;&nbsp;&nbsp;(0, 1)
&nbsp;&nbsp;&nbsp;&nbsp;'''else'''
&nbsp;&nbsp;&nbsp;&nbsp;'''let''' (a, b) = fibpom (n - 1)
&nbsp;&nbsp;&nbsp;&nbsp;'''in''' (b, a + b)
       
fib n =
&nbsp;&nbsp;'''let''' (a, b) = fibpom n
&nbsp;&nbsp;'''in''' a
 
W Scheme'ie:
 
('''define''' (fibpom n)
&nbsp;&nbsp;('''if''' (= n 0)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(cons 0 1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;('''let''' ((p (fibpom (- n 1))))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(cons (cdr p) (+ (car p) (cdr p))))))
('''define''' (fib n) (car (fibpom n)))
 
Procedura pomocnicza <tt>fibpom</tt> spełnia specyfikację: <tt>fibpom n</tt>&nbsp;<math>=(\mbox{Fib}_n, \mbox{Fib}_{n+1})</math>.
 
W trakcie wykładu poznamy inne sposoby definiowania funkcji obliczającej liczby Fibonacciego.


== Literatura ==
== Literatura ==
* H.Abelson, G.J.Sussman, ''Struktura i interpretacja programów komputerowych'', WNT 2002.
* [AS]  H. Abelson, G. J. Sussman, ''Struktura i interpretacja programów komputerowych'', [http://www.wnt.pl/product.php?action=0&prod_id=347&hot=1 WNT 2002].
* E.Chailloux, P.Manoury, B.Pagano,''Developing Applications with Objective Caml'',<br/>http://caml.inria.fr/oreilly-book/.
* [L]    X. Leroy, ''The Objective Caml system'', [http://caml.inria.fr/pub/docs/manual-ocaml/index.html]
* Didier Rémy, ''Using, Understanding and Unraveling the Ocaml Language'',<br/>http://caml.inria.fr/pub/docs/u3-ocaml/.
* [ChMP] E. Chailloux, P. Manoury, B. Pagano, ''Developing Applications with Objective Caml'', [http://caml.inria.fr/oreilly-book/]
* X.Leroy,''The Objective Caml system'',<br/>http://caml.inria.fr/pub/docs/manual-ocaml/index.html.
* [R]    D. Rémy, Using, ''Understanding and Unraveling the Ocaml Language'', [http://caml.inria.fr/pub/docs/u3-ocaml/]
* M.Benke, G.Grudziński, M.Konarski,''Tajny skrypt lab ML'',<br/>http://zls.mimuw.edu.pl/~mikon/ftp/TSLS.
* [GIH]  P. Hudak, J. Peterson, J. Fasel, ''A Gentle Introduction to Haskell'', [http://www.haskell.org/tutorial/]
* [RWH]  B. O'Sullivan, D. Stewart, J. Goerzen, ''Real World Haskell'', O'Reilly 2008, [http://book.realworldhaskell.org/]
 
== [[Programowanie funkcyjne/Wytyczne | Wytyczne dotyczące sposobu prowadzenia ćwiczeń]] ==
 
== [[Programowanie funkcyjne/Zadania egzaminacyjne|Propozycje zadań egzaminacyjnych]] ==


{{
== [[Programowanie funkcyjne/Tematy programów zaliczeniowych|Przykładowe tematy programów zaliczeniowych]] ==
Uwaga||uwaga_2|W niniejszych notatkach wykorzystano fragmenty innych pozycji poświęconych programowaniu funkcyjnemu, lub ogólnie programowaniu, a w szczególności z następujących pozycji: M.Benke, G.Grudziński, M.Konarski, ''Tajny skrypt lab SML'', A.Tarlecki, slajdy do wykładu z ''Programowania funkcyjnego'', M.Kubica, notatki do wykładu ze ''Wstępu do programowania (potok funkcyjny)'',publikacje ''Olimpiady Informatycznej'',podanej literatury.
}}


== Sprawy organizacyjne (CZY TO POWINNO TU BYC?) ==
== Podziękowania ==
Na zajęciach będziemy się posługiwać językiem Ocaml.
Niniejsze materiały powstały na podstawie notatek do prowadzonych przeze mnie, na przestrzeni kilku lat, wykładów z ''Programowania funkcyjnego'', oraz ze ''Wstępu do programowania'' i ''Metod programowania'' (w ramach ''potoku funkcyjnego'').
Zaliczenie wykładu polega na zdaniu (prostego) egzaminu pisemnego.
Chciałbym gorąco podziękować moim kolegom, którzy w tym czasie prowadzili ćwiczenia z tych przedmiotów, a w szczególności:
Zaliczenie laboratorium polega na napisaniu programu zaliczeniowego i
Jackowi Chrząszczowi,
jego "obronie".
Grzegorzowi Grudzińskiemu i
Program zaliczeniowy powinien być rozsądnej wielkości,
Mikołajowi Konarskiemu.
świadczący o opanowaniu praktycznym programowania funkcyjnego,
Ich uwagi miały wpływ na postać prowadzonych zajęć, a więc i również na te materiały.
podzielony na moduły, przejrzyście napisany i skomentowany.
W szczególności część zamieszczonych tu zadań pochodzi od nich.
Implementacja programu powinna w istotny sposób wykorzystywać
funkcyjny charakter języka (procedury wyższych rzędów, funktory,
strumienie, \dots).
Temat programu dowolny, ale zatwierdzony przez prowadzącego
laboratorium.  
"Obrona" polega na 10-cio minutowej prezentacji programu.

Aktualna wersja na dzień 22:17, 11 wrz 2023

Wstęp

Celem tego przedmiotu jest przedstawienie programowania funkcyjnego – paradygmatu i stylu programowania oraz towarzyszących mu technik programistycznych. W trakcie zajęć będziemy korzystać z języka progamowania Ocaml. (Nie jest to jednak w żadnej mierze kurs programowania w Ocamlu.) Zaczniemy od krótkiego intuicyjnego porównania paradygmatu programowania funkcyjnego oraz najpopularniejszego paradygmatu programowania – programowania imperatywnego. W kilku początkowych wykładach przedstawiamy podstawy programowania w Ocamlu. Następnie poznamy jedną z podstawowych koncepcji programowania funkcyjnego: procedury wyższych rzędów. W kolejnym wykładzie zaprezentujemy (uproszczoną) semantykę operacyjną podstawowego fragmentu języka. Następnie zajmiemy się modułami (strukturami), ich interfejsami (sygnaturami) oraz modułami sparametryzowanymi (funktorami). Dowiemy się też, jak w języku funkcyjnym mogą być zrealizowane konstrukcje imperatywne. Zostaną one następnie zastosowane do zaimplementowania spamiętywania i uleniwiania obliczeń. Te zaś techniki zostaną użyte do zaimplementowania strumieni. Na zakończenie przedstawimy krótki przegląd innych najpopularniejszych funkcyjnych języków programowania: Haskella i Scheme'a (dialektu Lispu).

Porównanie paradygmatu funkcyjnego i imperatywnego

Najbardziej popularny paradygmat programowania to programowanie imperatywne. Języki programowania takie, jak C, C++, Java, czy Pascal, to języki imperatywne. (Dodatkowo, część z nich to języki obiektowe.) Istotą programowania imperatywnego jest to, że program zawiera zmienne (lub obiekty) oraz operacje. Zmienne i obiekty charakteryzują się stanem, który może ulegać zmianie w czasie. Operacje służą wykonywaniu obliczeń, zmianie stanu zmiennych (przypisanie) lub obiektów oraz kontrolują przepływ sterowania w obrębie programu.

Paradygmat funkcyjny polega na tym, że pisząc program, tworzymy coraz bardziej skomplikowane pojęcia matematyczne, aż do osiągnięcia celu. Pojęcia te mają postać stałych i funkcji, stąd nazwa programowanie funkcyjne. Oczywiście tworzone funkcje są wykonywalne, tzn. dostarczając argumentów możemy obliczyć ich wartości.

Charakterystyczne dla programowania funkcyjnego jest również to, że funkcje są "obywatelami pierwszej kategorii", tzn. przysługują im wszystkie te prawa, co innego rodzaju wartościom. W tej chwili nie jest jeszcze jasne o jakie prawa może chodzić. Można jednak śmiało powiedzieć, że jest to jedna z fundamentalnych zasad programowania funkcyjnego. Jak zobaczymy, zasada ta jest jak konstytucja i wiele innych cech programowania funkcyjnego będzie z niej wynikać.

Porównajmy zastosowanie paradygmatów funkcyjnego i imperatywnego do tworzenia programów. Programowanie imperatywne jest powszechnie znane. Na czym jednak polega paradygmat imperatywny? W naszych programach zwykle staramy się wymodelować istotny dla nas fragment otaczającego nas świata. Świat ten składa się (na codzienny użytek) z przedmiotów. Przedmiotom tym odpowiadają w programach imperatywnych zmienne lub obiekty. Przedmioty w świecie rzeczywistym zmieniają się wraz z upływem czasu. Podobnie więc zmienne i obiekty obdarzone są stanem, który może się zmieniać w czasie. Dane są to pewne przedmioty, na których należy wykonywać określone czynności, w wyniku których przeistoczą się one w wyniki. Upływ czasu w świecie rzeczywistym jest modelowany przez upływ czasu w komputerze, a zmiany stanów są realizowane przez przypisania.

Pojęcie czasu nie jest tak bardzo obecne w programach funkcyjnych. Oczywiście, aby użyć jakiegoś pojęcia musimy je najpierw zdefiniować, a żeby uzyskać wynik funkcji, musimy najpierw dostarczyć argumentów. W "czystym" programowaniu funkcyjnym nie występuje jednak pojęcie zmiennej ani przypisania. Nie ma też pętli jako konstrukcji związanych z czasem i zmianą. Tworzenie programu polega na tworzeniu coraz to bardziej złożonych funkcji i stałych oraz na obliczaniu wartości wyrażeń.

Charakterystyka programowania funkcyjnego

Jeśli nie ma zmiennych, przypisania ani pętli, to co jest? Zamiast operacji mamy obliczanie wyrażeń. Zamiast przypisań mamy definiowanie stałych i funkcji (funkcje to w pewnym sensie też stałe, tylko o bardziej skomplikowanej naturze). Zamiast pętli mamy rekurencję.

Charakterystyczne dla programowania funkcyjnego jest to, że często bardzo łatwo jest napisać poprawny program, przepisując sformułowanie problemu, choć zwykle nie jest to najefektywniejszy program. Efektywny program jest dużo trudniejszy do napisania i nie zawsze jest tak przejrzysty jak jego nieefektywny odpowiednik. Mimo wszystko, łatwiej jest weryfikować programy funkcyjne niż imperatywne.

Specyfikacja programu może być mniej lub bardziej formalna. Jeśli jest sformalizowana, to zwykle ma postać relacji między danymi a wynikami. Program, który tworzymy, powinien mieścić się w ramach określonych przez specyfikację. Weryfikacja poprawności programu polega na sprawdzeniu, że faktycznie tak jest. W przypadku programów funkcyjnych zarówno specyfikacja jak i program mają postać obiektów matematycznych. Stąd łatwiej wykazać poprawność programu.

W przypadku programów imperatywnych, musimy wnioskować o zachodzących zmianach stanów zmiennych lub obiektów. Jeżeli mamy do czynienia z aliasingiem lub strukturami wskaźnikowymi, musimy wiedzieć, co ulega zmianie na skutek każdego przypisania, a co nie. Narażeni jesteśmy na błędy typu: czemu ta zmienna zmieniła wartość? czy te dwie instrukcje powinny być wykonywane w tej, czy odwrotnej kolejności? W przypadku programowania funkcyjnego takie problemy w ogóle nie powstają, co poprawia czytelność programów i ułatwia ich weryfikację.

Programowanie funkcyjne daje nam również do ręki pewne techniki programistyczne, które nie występują w imperatywnych językach programowania, jak: funkcje wyższych rzędów, strumienie, czy funktory. Poznamy je w trakcie tego wykładu.

Skłamałbym, gdybym twierdził, że programowanie funkcyjne jest pozbawione wad. Jedna z takich wad, którą zwykle programiści odczuwają boleśnie, jest brak (w czystym programowaniu funkcyjnym) imperatywnych tablic. Można się jednak bez nich obyć, czasami modyfikując algorytm, a w najgorszym przypadku zastępując je słownikowymi strukturami danych (zwanymi mapami), co zwiększa złożoność czasową o czynnik O(logn).

Programowanie imperatywne można porównać do klucza francuskiego, a programowanie funkcyjne do kombinerek. Są to dwa narzędzia o podobnym, ale nie identycznym, przeznaczeniu. Dobry programista powinien znać oba narzędzia i wiedzieć, kiedy które zastosować. Czasami należy użyć jednego z tych podejść, czasami drugiego, a czasami można je łączyć, tak aby tworzyć programy łatwo i tworzyć programy łatwe do zrozumienia.

Ocaml

W trakcie tego wykładu będziemy przede wszystkim korzystać z języka Ocaml (Objective Caml). Jest on dostępny pod adresem http://caml.inria.fr. Ocaml to język:

  • w pełni funkcyjny,
  • z polimorfizmem,
  • ze statycznym wiązaniem identyfikatorów,
  • ze (statyczną) kontrolą typów,
  • pozwalający na modularyzację programów,
  • zawierający konstrukcje imperatywne i wyjątki (tzn. konstrukcji imperatywnych można używać tylko w drodze wyjątku ;-),
  • obiektowy (z tego akurat nie będziemy korzystać),

Haskell

Innym popularnym językiem funkcyjnym jest Haskell ([1]). Haskell to język:

  • czysto funkcyjny (bez konstrukcji imperatywnych),
  • leniwy,
  • z polimorfizmem,
  • ze statycznym wiązaniem identyfikatorów,
  • ze (statyczną) kontrolą typów,
  • pozwalający na modularyzację programów,

Scheme

[2] to dialekt języka Lisp opracowany na MIT. Jest to język:

  • w pełni funkcyjny,
  • minimalistyczny,
  • z polimorfizmem,
  • z dynamicznym wiązaniem identyfikatorów,
  • z dynamiczną kontrolą typów,
  • zawierający konstrukcje imperatywne,

Kilka poglądowych przykładów

Przyjrzyjmy się kilku bardzo prostym przykładom. Skoro mówimy o programowaniu funkcyjnym, spróbujmy zaprogramować kilka prostych funkcji.

Uwaga
Od tej pory będziemy używać słowa funkcja na określenie pojęcia matematycznego, a słowa procedura na określenie pojęcia programistycznego


Wartość bezwzględna

Funkcję wyznaczającą wartość bezwzględną możemy opisać wzorem:

|x|={x; gdy x0x; gdy x<0

W Pascalu możemy ją zaimplementować w następujący sposób:

function abs(x : real) : real;
begin
  if x >= 0.0 then abs := x else abs := -x
end;

Natomiast w Ocamlu jej definicja będzie miała taką postać:

let abs x = 
  if x >= 0.0 then x else -. x;;

W Haskellu taką:

abs x | x > 0 = x
      | x < 0 = -x

A w Scheme'ie taką:

(define (abs x) 
  (if (>= x 0) x (- x)))


Zwróćmy uwagę na funkcyjny if. Nie obejmuje on instrukcji, ale wyrażenia. Powyższy kod w językach funkcyjnych jest bliższy podanemu wzorowi niż programowi w Pascalu.

Silnia

Wzór opisujący silnię ma postać:

0!=1(n+1)!=(n+1)n!

Oto iteracyjny program w Pascalu obliczający silnię:

function silnia (n : integer): integer;
var
  i, a : integer;
begin
  a := 1;
  for i := 1 to n do
     a := a * i;
  silnia := a
end;

W językach funkcyjnych iteracja jest wyrażona za pomocą rekurencji. W Ocamlu:

let rec silnia n = 
  if n < 2 then 1 else n * silnia (n - 1);;

W Haskellu:

silnia 0 = 1
silnia n = n * silnia (n - 1)

W Scheme'ie:

(define (factorial n) 
  (if (= n 0) 1 (* n (factorial (- n 1)))))

Znowu, program funkcyjny bardziej przypomina wzór niż program w Pascalu.

Liczby Fibonacciego

Wzór:

Fib0=0Fib1=1Fibn=Fibn2+Fibn1

Program w Pascalu:

function fib (n : integer) : integer;
var
  a, b, c, i : integer;
begin
  a := 0;
  b := 1;
  for i := 1 to n do begin
    c := a + b;
    a := b;
    b := c
  end;
  fib := a
end;

Program w Ocamlu:

let rec fib n = 
  if n < 2 then n else fib (n - 1) + fib (n - 2);;

W Haskellu:

fib n = if n < 2 then n else fib (n - 1) + fib (n - 2)

W Scheme'ie:

(define (fib n) 
  (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))

Taki program jest prostym przełożeniem wzoru na język programowania. Jest on jednak nieefektywny. Możemy zapisać program działający podobnie do powyższego programu w Pascalu, gdzie w każdym kroku iteracji pamiętamy dwie kolejne liczby Fibonacciego. Iteracja jest tu wyrażona za pomocą rekurencji.

W Ocamlu:

let rec fibpom n = 
  if n = 0 then
    (0, 1)
  else
    let (a, b) = fibpom (n - 1)
    in (b, a + b);;
let fib n =
  let (a, b) = fibpom n
  in a;;

W Haskellu:

fibpom n = 
  if n == 0 then 
    (0, 1) 
    else 
    let (a, b) = fibpom (n - 1)
    in (b, a + b)
       
fib n = 
  let (a, b) = fibpom n
  in a

W Scheme'ie:

(define (fibpom n)
  (if (= n 0) 
      (cons 0 1) 
      (let ((p (fibpom (- n 1))))
           (cons (cdr p) (+ (car p) (cdr p))))))

(define (fib n) (car (fibpom n)))

Procedura pomocnicza fibpom spełnia specyfikację: fibpom n =(Fibn,Fibn+1).

W trakcie wykładu poznamy inne sposoby definiowania funkcji obliczającej liczby Fibonacciego.

Literatura

  • [AS] H. Abelson, G. J. Sussman, Struktura i interpretacja programów komputerowych, WNT 2002.
  • [L] X. Leroy, The Objective Caml system, [3]
  • [ChMP] E. Chailloux, P. Manoury, B. Pagano, Developing Applications with Objective Caml, [4]
  • [R] D. Rémy, Using, Understanding and Unraveling the Ocaml Language, [5]
  • [GIH] P. Hudak, J. Peterson, J. Fasel, A Gentle Introduction to Haskell, [6]
  • [RWH] B. O'Sullivan, D. Stewart, J. Goerzen, Real World Haskell, O'Reilly 2008, [7]

Wytyczne dotyczące sposobu prowadzenia ćwiczeń

Propozycje zadań egzaminacyjnych

Przykładowe tematy programów zaliczeniowych

Podziękowania

Niniejsze materiały powstały na podstawie notatek do prowadzonych przeze mnie, na przestrzeni kilku lat, wykładów z Programowania funkcyjnego, oraz ze Wstępu do programowania i Metod programowania (w ramach potoku funkcyjnego). Chciałbym gorąco podziękować moim kolegom, którzy w tym czasie prowadzili ćwiczenia z tych przedmiotów, a w szczególności: Jackowi Chrząszczowi, Grzegorzowi Grudzińskiemu i Mikołajowi Konarskiemu. Ich uwagi miały wpływ na postać prowadzonych zajęć, a więc i również na te materiały. W szczególności część zamieszczonych tu zadań pochodzi od nich.