Programowanie funkcyjne/Procedury jeszcze wyższych rzędów: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Przemek (dyskusja | edycje)
Nie podano opisu zmian
m Zastępowanie tekstu – „<math> ” na „<math>”
 
(Nie pokazano 24 wersji utworzonych przez 3 użytkowników)
Linia 1: Linia 1:
==Wstęp==
==Wstęp==
<p align="justify">
<p align="justify">
Jaka jest różnica między danymi i procedurami? W programowaniu funkcyjnym to rozróżnienie rozmywa się.
Jaka jest różnica między danymi i procedurami?  
Dane mogą być traktowane jak procedury --- każdy interpreter czy kompilator zamienia dane (kod źródłowy programu) na procedurę
W programowaniu funkcyjnym to rozróżnienie rozmywa się.
(wykonywalny program). Procedury wyższych rzędów operują na innych procedurach jak na danych. Tak więc procedury mogą być również danymi. Można by powiedzieć, że dane tym różnią się od procedur, że zawsze są podmiotem obliczeń, a nigdy nie są wykonywane.
Dane mogą być traktowane jak procedury --- każdy interpreter czy kompilator zamienia dane (kod źródłowy programu) na procedurę (wykonywalny program).  
Niniejszy wykład powinien Państwa przekonać, że wcale tak nie musi być. Używając języka programowania nie widzimy w jaki sposób zrealizowane są struktury danych i na jak jest zrealizowane wykonywanie procedur. Łatwo sobie wyobrazić, że procedury mogą mieć postać kodu źródłowego lub częściowo skompilowanego, który jest ''interpretowany''.Tak więc procedury mogą być zrealizowane w postaci danych (interpretowanych przez procedurę ewaluatora).
Procedury wyższych rzędów operują na innych procedurach jak na danych.  
Tak więc procedury mogą być również danymi.  
Można by powiedzieć, że dane tym różnią się od procedur, że zawsze są podmiotem obliczeń, a nigdy nie są same wykonywane.
Niniejszy wykład powinien Państwa przekonać, że wcale tak nie musi być.  
</p>


Podobnie, dane mogą być procedurami. Nie widzimy sposobu implementacji podstawowych struktur danych wbudowanych w język programowania. Poznamy teraz jedną z możliwych ich implementacji - całkowicie proceduralną.  
<p align="justify">
Używając języka programowania nie widzimy, w jaki sposób zrealizowane są struktury danych, ani jak jest zrealizowane stosowanie procedur do argumentów.
Łatwo sobie wyobrazić, że procedury mogą mieć postać kodu źródłowego lub częściowo skompilowanego, który jest ''interpretowany''.
Jak zobaczymy w tym wykładzie, również dane nie muszą być biernym przedmiotem obliczeń.
Poznamy jedną z możliwych implementacji danych --- całkowicie proceduralną.  
</p>
</p>


{{
<p align="justify">
uwaga||uwaga_10|Niniejszy wykład ma charakter ćwiczenia umysłowego. Struktury danych wcale nie są implementowane w opisany sposób, ani nie jest to najlepszy sposób ich implementacji. Jest to jednak możliwe. Celem tego ćwiczenia jest zilustrowanie w pełni zamiennego traktowania danych jak procedur i procedur jak danych. Zdobywszy tę umiejętność będziemy mogli wznieść się na wyższy poziom abstrakcji i tworzyć struktury, w których dane są przemieszane z procedurami.
Poniżej pokazujemy, jak można zaimplementować podstawowe struktury danych korzystając jedynie z procedur wyższych rzędów.
</p>


{{uwaga||uwaga_10|
'''Niniejszy wykład ma całkowicie charakter ćwiczenia umysłowego.'''
Struktury danych wcale nie są implementowane w opisany sposób, ani nie jest to najlepszy sposób ich implementacji.
Jednak opisany sposób ich realizacji jest możliwy.
Celem tego ćwiczenia jest zilustrowanie w pełni zamiennego traktowania danych i procedur.
Zdobywszy tę umiejętność, będziemy mogli wznieść się na wyższy poziom abstrakcji i tworzyć struktury,
w których dane są przemieszane z procedurami.
Jest to również dobre ćwiczenie rozwijające umiejętność posługiwania się procedurami wyższych rzędów.
}}
}}


Niektóre programy, ze względu na system typów Ocamla działają "jednorazowo", tzn. każda konstrukcja jest poprawna izostała przetestowana, jednak użycie jednej konstrukcji może uniemożliwić użycie innej.
==Definiowanie form specjalnych==
<p align="justify">
<p align="justify">
W Ocamlu argumenty procedur są obliczane ''gorliwie'',tzn. najpierw są obliczane ich wartości, a dopiero potem przystępujemy do obliczania wartości procedury. Wyjątkiem są ''formy specjalne'', np. <tt>if</tt> ---w zależności od wartości warunku, tylko jeden z pozostałych członów jest obliczany. Czy można takie formy zaimplementować samemu? Tak. Potrzeba do tego dwóch rzeczy. Musimy umieć zabezpieczać wartości przed zbyt wczesnym obliczaniem i definiować ''makrodefinicje''.
Pokazane tutaj konstrukcje pochodzą z [http://en.wikipedia.org/wiki/Lambda_calculus <math>\lambda</math>-rachunku],  
gdzie faktycznie mają zastosowanie.  
Służą one do zaimplementowania podstawowych typów danych i operacji na nich.
Robi się to w ramach dowodu, że w <math>\lambda</math>-rachunku można zapisać dowolne funkcje obliczalne.  
</p>
</p>
===Makrodefinicje===
 
==Wartości logiczne==
<p align="justify">
<p align="justify">
W Ocamlu możemy zmieniać składnię języka wprowadzając własne makrodefinicje. Służy do tego preprocesor P4.  Nie będziemy tu mówić o nim, lecz sporadycznie użyjemy definicji wprowadzających potrzebne nam formy specjalne.
Aby mówić o wartościach logicznych, potrzebujemy zaimplementować następujące pojęcia i konstrukcje językowe:
* prawdę i fałsz,
* wyrażenia warunkowe <tt>if-then-else</tt>,
* operacje logiczne: koniunkcję, alternatywę i negację.
</p>
</p>


==Uleniwianie==
<p align="justify">
<p align="justify">
Model obliczeniowy Ocamla charakteryzuje się zachłannym obliczaniem wartości --- argumenty funkcji są obliczane bez względu na to, czy są potrzebne. Uleniwianie polega na zabezpieczeniu wartości przed obliczeniem, jeżeli nie jest to potrzebne. Zamiast wartości mamy "obietnicę jej obliczenia", czyli procedurę bezargumentową, której wartością jest dana wartość. W momencie gdy wartość jest nam potrzebna, obliczamy ją. Żeby nie obliczać tej wartości wielokrotnie, stosujemy spamiętywanie. W Ocamlu dostępna jest forma specjalna <tt>lazy</tt> i procedura <tt>force</tt>. Lazy powoduje odroczenie (ze spamiętywaniem) obliczenia wyrażenia.
Mamy dwie wartości logiczne: prawdę i fałsz.
Chcąc obliczyć wartość wykonujemy na wyniku <tt>lazy</tt> operację <tt>force</tt>. Na potrzeby tego wykładu zaimplementujemy uproszczoną wersję uleniwiania, bez spamiętywania. Do zaimplementowania spamiętywania potrzebowalibyśmy operacji
Wartość logiczną możemy reprezentować jako procedurę dwuargumentową, której wynikiem jest jeden z jej argumentów.  
imperatywnych, o których nie było jeszcze mowy.  
W przypadku prawdy wybiera pierwszy, a w przypadku fałszu drugi argument.
</p>
</p>
        
        
  '''type''' 'a delayed = unit -> 'a;;
  '''let''' prawda x y = x;;
  '''lazy''' w &equiv; '''function''' () -> w
  ''val prawda : 'a -> 'b -> 'a = <fun>''
  '''let''' force x = x ();;
  '''let''' a = delay 4;;
  '''let''' falsz  x y = y;;
'''force''' a;;
  ''val falsz : 'a -> 'b -> 'b = <fun>''


==Wartości logiczne==
<p align="justify">
Aby mówić o wartościach logicznych potrzebujemy:
Wyrażenia warunkowe <tt>if-then-else</tt> możemy zrealizować za pomocą trójargumentowej procedury <tt>jesli</tt>.
* prawdę i fałsz,
Jej pierwszym argumentem jest wartość logiczna, a dwa pozostałe argumenty to odroczone wartości.
* <tt>(if ...)</tt>,
(Używamy tu odroczonych wartości bez spamiętywania, tak jak zostały one zaimplementowane w poprzednim wykładzie.)
* koniunkcję, alternatywę i negację.
Procedura ta, w zależności od wartości warunku, wybiera jedną z odroczonych wartości i oblicza ją.
</p>


Wartość logiczną reprezentuję jako procedurę dwuargumentową, która w przypadku prawdy wybiera pierwszy, a w przypadku fałszu drugi argument.
     
'''let''' prawda x y = x;;
'''let''' falsz  x y = y;;
  '''let''' jesli w k a = force (w k a);;
  '''let''' jesli w k a = force (w k a);;
''val jesli : ('a -> 'b -> unit -> 'c) -> 'a -> 'b -> 'c = <fun>''
  jesli prawda (lazy 1) (lazy 2);;
  jesli prawda (lazy 1) (lazy 2);;
  ''= 1''
  ''- : int = 1''
 
<p align="justify">
Implementując koniunkcję i alternatywę korzystamy z następujących tożsamości:
* ''prawda'' <math>\wedge x \equiv x</math>,
* ''fałsz'' <math>\wedge x \equiv</math> ''fałsz'',
* ''prawda'' <math>\vee x \equiv</math> ''prawda'',
* ''fałsz'' <math>\vee x \equiv x</math>.
Negacja polega na zamianie rolami dwóch argumentów wartości logicznej.
</p>
 
  '''let''' i x y  = x y falsz;;
  '''let''' i x y  = x y falsz;;
''val i : ('a -> ('b -> 'c -> 'c) -> 'd) -> 'a -> 'd = <fun>''
  '''let''' lub x y = x prawda y;;
  '''let''' lub x y = x prawda y;;
''val lub : (('a -> 'b -> 'a) -> 'c -> 'd) -> 'c -> 'd = <fun>''
  '''let''' nie x a b = x b a;;
  '''let''' nie x a b = x b a;;
''val nie : ('a -> 'b -> 'c) -> 'b -> 'a -> 'c = <fun>''
  jesli (lub falsz prawda) (lazy 1) (lazy 2);;
  jesli (lub falsz prawda) (lazy 1) (lazy 2);;
  ''= 1''
  ''- : int = 1''


==Produkty kartezjańskie==
==Produkty kartezjańskie==
<p align="justify">
Zaimplementujemu tutaj produkty kartezjańskie dwóch typów.
Produkty kartezjańskie większej liczby typów i rekordy nie różnią się istotnie.
</p>
<p align="justify">
Aby mówić o produkcie musimy mieć:
Aby mówić o produkcie musimy mieć:
* konstruktor pary <tt>pr: <math> \alpha \to \beta \to</math> produkt</tt>,
* konstruktor pary <tt>para: 'a -> 'b -> ('a * `b)</tt>,
* rzuty: <tt>p1: produkt <math> \to \alpha</math></tt> i <tt>p2: produkt <math> \to \beta</math></tt>.
* rzuty na współrzędne: <tt>p1: ('a * 'b) -> 'a</tt> i <tt>p2: ('a * 'b) -> 'b</tt>.
</p>


Produkt, to para argumentów czekających na funkcję.
<p align="justify">
Jak można zaimplementować parę nie używając żadnej struktury danych?
Jedyna para o jakiej możemy mówić, to para argumentów procedury.
Nam jednak potrzebna jest para bez procedury.
Możemy jednak zdefiniować parę argumentów bez procedury -- parę argumentów czekających na procedurę, której zostaną przekazane.
To bedzie nasza proceduralna implementacja pary.  
</p>
        
        
  '''let''' pr x y = '''function''' f -> f x y;;
  '''let''' para x y = '''function''' f -> f x y;;
''val para : 'a -> 'b -> ('a -> 'b -> 'c) -> 'c = <fun>''
 
<p align="justify">
Zauważmy, że rzutowanie jest bardzo proste.
Wystarczy przekazać parze procedurę, która wybierze pierwszy lub drugi z argumentów, które otrzyma.
Takie procedury już znamy, są to: <tt>prawda</tt> i <tt>falsz</tt>.
</p>
 
  '''let''' p1 p = p prawda;;
  '''let''' p1 p = p prawda;;
''val p1 : (('a -> 'b -> 'a) -> 'c) -> 'c = <fun>''
  '''let''' p2 p = p falsz;;
  '''let''' p2 p = p falsz;;
  '''let''' p x = pr 1 "ala" x;;
''val p2 : (('a -> 'b -> 'b) -> 'c) -> 'c = <fun>''
  '''let''' p x = para 1 "ala" x;;
''val p : (int -> string -> 'a) -> 'a = <fun>''
  p1 p;;
  p1 p;;
  ''= 1''
  ''- : int = 1''
  p2 p;;
  p2 p;;
  ''= "ala"''
  ''- : string = "ala"''
 
{{uwaga|||
Tajemniczy parametr <tt>x</tt> w powyższym przykładzie jest wynikiem zastosowania [http://en.wikipedia.org/wiki/Eta_reduction#.CE.B7-conversion eta-konwersji] i ma na celu uniknięcie wprowadzenia w typie <tt>p</tt> słabych zmiennych typowych. Jeśli Czytelnik nie widzi jego celowości, nie należy się tym przejmować. }}
 
== Listy ==
<p align="justify">
Zaimplementujemy tutaj listy nieskończone.
Listy skończone można łatwo zaimplementować jako pary: długość listy + lista nieskończona.
</p>
 
<p align="justify">
Listy nieskończone można reprezentować jako procedury jednoargumentowe.
Argumentem takiej procedury może być liczba naturalna, indeks elementu listy, a jej wynikiem jest wartość elementu listy.
(To jak zaimplementować liczby naturalne pokazujemy dalej.)
</p>
 
<p align="justify">
Musimy zaimplementować:
* konstruktor nieskończonej listy stałej,
* dołączenie danego elementu na początku danej listy,
* procedurę zwracającą pierwszy element (głowę) danej listy,
* procedurę zwracającą ogon danej listy.
</p>


==Listy nieskończone==
Listy nieskończone można reprezentować jako funkcje z <tt>int</tt>. Potrzebujemy:
* listę stałą,
* dołączenie elementu na początek listy,
* pierwszy element listy,
* ogon listy.
     
  '''let''' stala x n =  x;;
  '''let''' stala x n =  x;;
''val stala : 'a -> 'b -> 'a = <fun>''
  '''let''' sklej h t n = '''if''' n = 0 '''then''' h '''else''' t (n-1);;  
  '''let''' sklej h t n = '''if''' n = 0 '''then''' h '''else''' t (n-1);;  
''val sklej : 'a -> (int -> 'a) -> int -> 'a = <fun>''
  '''let''' glowa l = l 0;;
  '''let''' glowa l = l 0;;
''val glowa : (int -> 'a) -> 'a = <fun>''
  '''let''' ogon l n = l (n + 1);;
  '''let''' ogon l n = l (n + 1);;
''val ogon : (int -> 'a) -> int -> 'a = <fun>''


==Liczby naturalne Churcha==
== Liczby naturalne Churcha ==
Pozostały nam jeszcze liczby naturalne. (Rozszerzenie liczb naturalnych do całkowitych pozostawiamy jako ćwiczenie dla ambitnych. :-) Utożsamiamy liczbę naturalną z liczbą iteracji zadanej funkcji.
<p align="justify">
Pozostały nam jeszcze liczby naturalne.  
(Rozszerzenie liczb naturalnych do całkowitych pozostawiamy jako ćwiczenie dla ambitnych czytelników. :-)  
Będziemy utożsamiać liczbę naturalną z liczbą iteracji zadanej funkcji.
Dokładniej, liczbę <math>n</math> implementujemy jako procedurę,
której argumentem jest funkcja (nazwijmy ją <math>f</math>), a wynikiem <math>f^n</math>.
</p>
 
<center><math>n\ \equiv\ f \to f^n</math></center>
 
<p align="justify">
Używając tej analogii, zero reprezentujemy jako funkcję zwracającą identyczność, a jedynkę jako identyczność (na funkcjach).
</p>


<center><math> n\ \equiv\ f \to f^n </math></center>
     
  '''let''' id x = x;;
  '''let''' id x = x;;
''val id : 'a -> 'a = <fun>''
  '''let''' compose f g = '''function''' x -> f (g x);;
  '''let''' compose f g = '''function''' x -> f (g x);;
''val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>''
  '''let''' zero f = id;;
  '''let''' zero f = id;;
  '''let''' jeden f = f;;
''val zero : 'a -> 'b -> 'b = <fun>''
     
Dodawanie i mnożenie:
  '''let''' jeden f x = f x;;
{| border="0" cellspacing="0" cellpadding="5"
''val jeden : ('a -> 'b) -> 'a -> 'b = <fun>''
|-  
 
|
 
<code>'''let''' inc n f = compose (n f) f;;</code>
<p align="justify">
|
Dodawanie i mnożenie definiujemy korzystając z poniższych toższamości:
<math> f^{n+1} = f^n \circ f</math><br/>
</p>
|-
 
|
 
  <code>'''let''' plus m n f = compose (m f) (n f);;</code>
<math>f^{n+1} = f^n \circ f</math>
|
  '''let''' inc n f = compose (n f) f;;
<math> f^{m+n} = f^m \circ f^n</math><br/>
''val inc : (('a -> 'b) -> 'b -> 'c) -> ('a -> 'b) -> 'a -> 'c = <fun>''
|-
 
|
 
  <code>'''let''' razy = compose;;</code>
<math>f^{m+n} = f^m \circ f^n</math>
|
  '''let''' plus m n f = compose (m f) (n f);;
<math> (\texttt{razy}\ m\ n)\ f = (\texttt{compose}\ m\ n)\ f =</math> <br/>
''val plus : ('a -> 'b -> 'c) -> ('a -> 'd -> 'b) -> 'a -> 'd -> 'c = <fun>''
<math>  m (n\ f) = m (f^n) = (f^n)^m = f^{m \cdot n} </math>
 
|-
 
|}
<math>f^{m \cdot n} = (f^n)^m = m (f^n) = m (n\ f) = (m \circ n)\ f</math>
'''let''' razy = compose;;
 


<br/>
Do celów testowych możemy używać następujących procedur konwertujących liczby proceduralne na <tt>int</tt> i odwrotnie:
Do celów testowych można używać:
        
        
  '''let''' ile n = n ('''function''' x -> x + 1) 0;;
  '''let''' ile n = n ('''function''' x -> x + 1) 0;;
  '''let''' compose f g = '''function''' x -> f (g x);;
  ''val ile : ((int -> int) -> int -> 'a) -> 'a = <fun>''
  '''let''' '''rec''' iterate n f =
  '''let''' '''rec''' iterate n f =
&nbsp;&nbsp;'''if''' n = 0 '''then''' id '''else''' compose (iterate (n-1) f) f;;
  '''if''' n = 0 '''then''' id '''else''' compose (iterate (n-1) f) f;;
''val iterate : int -> ('a -> 'a) -> 'a -> 'a = <fun>''
  ile (iterate 1000);;
  ile (iterate 1000);;
  ''= 1000''
  ''- : int = 1000''
&nbsp;&nbsp;
 
  '''let''' dwa x = plus jeden jeden x;;
  '''let''' dwa x = plus jeden jeden x;;
''val dwa : ('a -> 'a) -> 'a -> 'a = <fun>''
  ile (razy dwa dwa);;
  ile (razy dwa dwa);;
  ''= 4''       
  ''- : int = 4''       


Jak moglibyśmy odróżniać od siebie liczby naturalne Churcha? Podstawową operacją porównania jest tzw. test zera ---
sprawdzenie, czy dana liczba jest równa zero. Szukamy więc takiej funkcji <math> f</math>, że <math> f^0 = \texttt{id}</math> jest czymś istotnie innym, niż <math> f^i</math> dla <math> i > 0</math>. Taką funkcją może być:


<center><math> f(x) = \texttt{falsz} </math></center>
<p align="justify">
     
Jak moglibyśmy odróżniać od siebie liczby naturalne Churcha?
  '''let''' test_zera x = x ('''function''' _ -> fałsz) prawda;;
Podstawową operacją porównania jest tzw. test zera --- sprawdzenie, czy dana liczba jest równa zero.
Szukamy więc takiej funkcji <math>f</math>, że <math>f^0 = \texttt{id}</math> jest czymś istotnie innym,
niż <math>f^i</math> dla <math>i > 0</math>.
Taką funkcją może być n.p.: <math>f(x) = \texttt{falsz}</math>.
Wówczas <math>f^0</math> <tt>prawda = prawda</tt>, natomiast
<math>f^i</math> <tt>prawda = falsz</tt> (dla <math>i > 0</math>).
</p>
 
  '''let''' test_zera x = x ('''function''' _ -> falsz) prawda;;
''val test_zera : (('a -> 'b -> 'c -> 'c) -> ('d -> 'e -> 'd) -> 'f) -> 'f = <fun>''
  jesli (test_zera zero) (lazy 1) (lazy 2);;
  jesli (test_zera zero) (lazy 1) (lazy 2);;
  ''= 1''       
  ''- : int = 1''       
 


Jak zmniejszyć <math> n</math> o 1? Liczba <math> n</math> to taki obiekt, który przekształca zadaną funkcję <math> f</math>
<p align="justify">
w funkcję <math> f^n</math>. Nie mamy jednak żadnego dostępu do tego jak <math> n</math> to robi. Problem ten można przedstawić sobie tak: mając dane <math> n</math>, <math> f</math> i <math> x</math> należy obliczyć <math> f^{n-1}(x)</math>.  
Jak zmniejszyć daną liczbę <math>n</math> o 1?  
Liczba <math>n</math> to taki obiekt, który przekształca zadaną funkcję <math>f</math> w funkcję <math>f^n</math>.  
Nie mamy jednak żadnego dostępu do tego jak <math>n</math> to robi.  
Problem ten można przedstawić sobie tak: mając dane <math>n</math>, <math>f</math> i <math>x</math> należy obliczyć  
<math>f^{n-1}(x)</math>.  
</p>


Rozważmy funkcję:
Rozważmy funkcję:
<center><math> g(a, b) = (b, f\ a) </math></center>
 
<center><math> g(a, b) = (b, f\ b)</math></center>


oraz ciąg:
oraz ciąg:
<center><math>  (g^i(x,x))_{i=0,\dots,n} = ((x,x), (x, f\ x), (f\ x, f^2\ x), \dots, (f^{n-1}\ x, f^n\ x)) </math></center>


<br/>
<center><math> (g^i(x,x))_{i=0,\dots,n} = ((x,x), (x, f\ x), (f\ x, f^2\ x), \dots, (f^{n-1}\ x, f^n\ x))</math></center>
Wystarczy więc z ostatniej pary w ciągu wydobyć pierwszą współrzędną. Co można zapisać tak:
 
     
<p align="justify">
Wystarczy więc z ostatniej pary w ciągu, <math>g^n(x, x) = (f^{n-1}\ x, f^n\ x)</math> wydobyć pierwszą współrzędną.  
Można to zapisać tak:
</p>
 
  '''let''' dec n f x =  
  '''let''' dec n f x =  
&nbsp;&nbsp;'''let''' g (a, b) = (b, f b)
  '''let''' g (a, b) = (b, f b)
&nbsp;&nbsp;'''in''' '''let''' (y, _) = n g (x, x)  
  '''in''' '''let''' (y, _) = n g (x, x)  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''in''' y;;
      '''in''' y;;
  '''''val''' dec:(('a*'b->'b*'c)->'d*'d->'e*'f)->('b->'c)->'d->'e''       
  ''val dec:(('a*'b->'b*'c)->'d*'d->'e*'f)->('b->'c)->'d->'e''       


lub tak, wykorzystując zaimplementowane przez nas produkty kartezjańskie:
lub tak, wykorzystując zaimplementowane przez nas produkty kartezjańskie:
        
        
  '''let''' dec n f x = p1 (n (function p -> pr (p2 p) (f (p2 p))) (pr x x));;
  '''let''' dec n f x = p1 (n ('''function''' p -> para (p2 p) (f (p2 p))) (para x x));;
  ''val dec : ''
  ''val dec : (((('a -> 'b -> 'b) -> 'c) -> ('c -> 'd -> 'e) -> 'e) -> (('f -> 'f -> 'g) -> 'g) -> ('h -> 'i -> 'h) -> 'j) -> ('c -> 'd) -> 'f -> 'j = <fun>''
''(((('a -> 'b -> 'b) -> 'c) -> ('c -> 'd -> 'e) -> 'e) -> ''
 
''(('f -> 'f -> 'g) -> 'g) -> ('h -> 'i -> 'h) -> 'j) -> ''
''('c -> 'd) -> 'f -> 'j = <fun>''
  ile (dec dwa);;
  ile (dec dwa);;
  ''= 1''       
  ''- : int = 1''       


Odejmowanie, to wielokrotne zmniejszanie o jeden:
Odejmowanie to wielokrotne zmniejszanie o jeden:
        
        
  '''let''' minus m n = (n dec) m;;
  '''let''' minus m n = (n dec) m;;
''val minus : 'a -> (((('b * 'c -> 'c * 'd) -> 'e * 'e -> 'f * 'g) -> ('c -> 'd) -> 'e -> 'f) -> 'a -> 'h) -> 'h = <fun>''
  ile (minus dwa jeden);;
  ile (minus dwa jeden);;
  ''= 1''       
  ''- : int = 1''       


<p align="justify">
Zwróćmy uwagę, że nie mamy liczb ujemnych, a w wyniku odjęcia od mniejszej liczby większej otrzymujemy zawsze zero.
Za pomocą odejmowania i testu zera można zaimplementować porównanie:
Za pomocą odejmowania i testu zera można zaimplementować porównanie:
     
</p>
 
  '''let''' wieksze m n = nie (test_zera (minus m n));;
  '''let''' wieksze m n = nie (test_zera (minus m n));;
''val wieksze : 'a -> (((('b * 'c -> 'c * 'd) -> 'e * 'e -> 'f * 'g) -> ('c -> 'd) -> 'e -> 'f) -> 'a -> ('h -> 'i -> 'j -> 'j) -> ('k -> 'l -> 'k) -> 'm -> 'n -> 'o) -> 'n -> 'm -> 'o = <fun>''
  jesli (wieksze dwa jeden) (lazy 1) (lazy 2);;
  jesli (wieksze dwa jeden) (lazy 1) (lazy 2);;
  ''= 1''
  ''- : int = 1''


==Abstrakcja rekurencji==
==Abstrakcja rekurencji==
Procedura <tt>fold</tt> stanowi abstrakcję rekurencyjnego przetwarzanialist. Czy istnieje kwintesencja wszelkiej rekurencji?
<p align="justify">
A co to jest rekurencja? Jak przerobić definicję rekurencyjną na nierekurencyjną --- poprzez wprowadzenie dodatkowego parametru funkcyjnego. Przykład: silnia. Z rekurencją:
Zastanowimy się teraz nad następującym problemem.
Procedury <tt>fold_left</tt> i <tt>fold_right</tt> stanowią abstrakcję rekurencyjnego przetwarzanialist.  
Czy istnieje abstrakcja wszelkiej rekurencji?
Nasze zadanie możemy sformułować tak: czy istnieje jedna konstrukcja, za pomocą której możemy zdefiniować dowolne procedury rekurencyjne, bez (dalszego) używania rekurencji?
Odpowiedź brzmi: tak.
</p>
 
<p align="justify">
Jak można przerobić rekurencyjną definicję procedury na nierekurencyjną ---  
poprzez wprowadzenie dodatkowego parametru funkcyjnego.
Zobaczmy to na przykładzie procedury obliczającej silnię.
Procedura rekurencyjna ma postać:
</p>
        
        
  '''let''' '''rec''' fac n =  
  '''let''' '''rec''' factorial n =  
&nbsp;&nbsp;'''if''' n <= 1 '''then'''  
  '''if''' n <= 1 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;1  
    1  
&nbsp;&nbsp;'''else'''
  '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;n * fac (n - 1);;
    n * factorial (n - 1);;
     
''val factorial : int -> int = <fun>''
Bez rekurencji:
 
<p align="justify">
Jeżeli usuniemy słowo kluczowe <tt>'''rec'''</tt>, to wewnątrz definicji nie będziemy mogli wywoływać procedury <tt>factorial</tt>.
Możemy jednak dodać dodatkowy parametr proceduralny <tt>fac</tt> i zamiast wywołania rekurencyjnego wywoływać procedurę przekazywaną przez ten parametr.
</p>
        
        
  '''let''' factorial fac n =  
  '''let''' factorial fac n =  
&nbsp;&nbsp;'''if''' n <= 1 '''then'''  
  '''if''' n <= 1 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;1  
    1  
&nbsp;&nbsp;'''else'''  
  '''else'''  
&nbsp;&nbsp;&nbsp;&nbsp;n * fac (n - 1);;
    n * fac (n - 1);;
     
''val factorial : (int -> int) -> int -> int = <fun>''
To jest procedura, która przetwarza procedurę <tt>fac</tt>liczącą poprawnie silnię dla liczb <math> \le n</math> na procedurę liczącą poprawnie silnię dla liczb <math> \le n + 1</math>. Teraz trzeba jakoś chytrze podstawić funkcję wynikową jako argument.
 
Inaczej mówiąc szukamy funkcji, która jest punktem stałym <tt>factorial</tt>.Będzie ona poprawnie liczyć silnię dla wszystkich liczb.  
<p align="justify">
Przyjrzyjmy się bliżej tak zdefiniowanej procedurze.
Można powiedzieć, że przetwarza ona procedurę <tt>fac</tt>, która poprawnie liczy silnię dla liczb  
<math>0, 1, \dots, n</math> na procedurę, która poprawnie liczy silnię dla liczb <math>0, 1, \dots, n + 1</math>.  
Jeżeli natomiast <tt>fac</tt> byłoby procedurą poprawnie liczącą silnię dla wszystkich liczb naturalnych,
to <tt>factorial fac</tt> również byłoby taką procedurą.  
Tak więc chcemy skonstruować procedurę obliczającą funkcję, która byłaby ''punktem stałym'' przekształcenia <tt>factorial</tt>.
Będzie ona poprawnie liczyć silnię dla wszystkich liczb.  
</p>
 
<p align="justify">
Szukamy więc czegoś, co się nazywa ''operatorem punktu stałego'' dla procedur przekształcających procedury w procedury.
Czytelnicy zaznajomieni z [[Semantyka i weryfikacja programów | ''Semantyką i weryfikacją programów'']]
zapewne zauważyli, że szukamy (w odpowiedniej dziedzinie) kresu górnego ciągu:
</p>
(function n -> <math>\perp</math>)
factorial (function n -> <math>\perp</math>)
factorial (factorial (function n -> <math>\perp</math>))
factorial (factorial (factorial (function n -> <math>\perp</math>)))
    <math>\vdots</math>


Operator punktu stałego --- pierwsze podejście.
<p align="justify">
Załóżmy, że <tt>f</tt> jest procedurą przekształcającą procedurę w procedurę (takiego samego typu).
W pierwszym podejściu, moglibyśmy spróbować zdefiniować operator punktu stałego następująco:
</p>
        
        
  '''let''' '''rec''' y f = f (y f);;
  '''let''' '''rec''' y f = f (y f);;
  &nbsp;&nbsp;
  ''val y : ('a -> 'a) -> 'a = <fun>''
 
  '''let''' f = y factorial ;;
  '''let''' f = y factorial ;;
  ''= factorial (y factorial) = factorial (factorial (y factorial)) = ...''       
  ''= factorial (y factorial) = factorial (factorial (y factorial)) = ...''       


To się niestety zapętli, ze względu na to, że argument procedury <tt>factorial</tt> jest obliczany przed jej zastosowaniem. Aby uniknąć zapętlenia, musimy uleniwić ten argument argument i wyliczać go tylko na żądanie. Operator punktu stałego.
 
<p align="justify">
Jednak ze względu na "gorliwe" obliczanie arguemntów procedur w Ocamlu, argument procedury <tt>factorial</tt> jest obliczany przed jej zastosowaniem, co powoduje zapętlenie obliczenia.  
Aby uniknąć zapętlenia, należy uleniwić ten argument i wyliczać go tylko na żądanie.  
Oto drugie podejście do implementacji operatora punktu stałego:
</p>
        
        
  '''let''' factorial fac n =  
  '''let''' factorial fac n =  
&nbsp;&nbsp;'''if''' n <= 1 '''then'''  
  '''if''' n <= 1 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;1  
    1  
&nbsp;&nbsp;'''else'''  
  '''else'''  
&nbsp;&nbsp;&nbsp;&nbsp;n * (force fac (n - 1));;
    n * (force fac (n - 1));;
  &nbsp;&nbsp;
  ''val factorial : (int -> int) Lazy.t -> int -> int = <fun>''
 
  '''let''' '''rec''' y f = f (lazy (y f));;
  '''let''' '''rec''' y f = f (lazy (y f));;
  &nbsp;&nbsp;
  ''val y : ('a lazy_t -> 'a) -> 'a = <fun>''
  '''let''' fac = y factorial ;;
  '''let''' fac = y factorial ;;
''val fac : int -> int = <fun>''


<center>
<center>
<math> \begin{matrix}\texttt{fac}\ 3 = (\texttt{y\ factorial})\ 3 = \\= (\texttt{factorial} (\texttt{lazy} (\texttt{y\ factorial})))\ 3 =\\= 3 * (\texttt{force} (\texttt{lazy} (\texttt{y\ factorial}))\ 2) =\\= 3 * (\texttt{y\ factorial}\ 2) = \\
<math>\begin{matrix}\texttt{fac}\ 3 = (\texttt{y\ factorial})\ 3 = \\= (\texttt{factorial} (\texttt{lazy} (\texttt{y\ factorial})))\ 3 =\\= 3 * (\texttt{force} (\texttt{lazy} (\texttt{y\ factorial}))\ 2) =\\= 3 * (\texttt{y\ factorial}\ 2) = \\
=3 * (\texttt{factorial} (\texttt{lazy} (\texttt{y\ factorial}))\ 2) = \\= 3 * (2 * (\texttt{force} (\texttt{lazy} (\texttt{y\ factorial})))\ 1) = \\= 3 * (2 * (\texttt{y\ factorial}\ 1)) =  \\= 3 * (2 * (\texttt{factorial} (\texttt{lazy} (\texttt{y\ factorial}))\ 1)) = \\= 3 * (2 * 1) = 6 \end{matrix}</math>
=3 * (\texttt{factorial} (\texttt{lazy} (\texttt{y\ factorial}))\ 2) = \\= 3 * (2 * (\texttt{force} (\texttt{lazy} (\texttt{y\ factorial})))\ 1) = \\= 3 * (2 * (\texttt{y\ factorial}\ 1)) =  \\= 3 * (2 * (\texttt{factorial} (\texttt{lazy} (\texttt{y\ factorial}))\ 1)) = \\= 3 * (2 * 1) = 6 \end{matrix}</math>
</center>
</center>


Każdą procedurę rekurencyjną możemy sprowadzić do zastosowania operatora punktu stałego i uleniwiania. Oto kolejny przykład, rekurencyjna procedura obliczająca liczby Fibonacciego:
<p align="justify">
     
Operator punktu stałego może być zastosowany nie tylko do obliczania silni.
  '''let''' fibonacci fib n =  
Za jego pomocą można każdą procedurę rekurencyjną zdefiniować bez używania rekurencji.
&nbsp;&nbsp;'''if''' n <= 2 '''then'''  
Powiedzmy, że nasza procedura rekurencyjna nazywa się <tt>p</tt>.
&nbsp;&nbsp;&nbsp;&nbsp;1
Należy w tym celu:
&nbsp;&nbsp;'''else'''
* zmienić nazwę definiowanej procedury, powiedzmy na <tt>p_y</tt>,
&nbsp;&nbsp;&nbsp;&nbsp;(force fib (n-1)) + (force fib (n-2));;
* dodać jej dodatkowy (pierwszy) parametr, nazwijmy go <tt>f</tt>,
  &nbsp;&nbsp;
* każde odwołanie rekurencyjne do procedury <tt>p</tt> zastąpić przez <tt>force f</tt>,
  '''let''' fib = y fibonacci;;
* zdefinować <tt>p</tt> jako <tt>y p_y</tt>.
Oto przykład zastosowania tej techniki do rekurencyjnej procedury obliczającej liczby Fibonacciego:
</p>
 
  '''let''' fib_y f n =  
  '''if''' n <= 2 '''then'''  
    1
  ''else'''
    (force f (n-1)) + (force f (n-2));;
   
  '''let''' fib = y fib_y;;


<center><math> \begin{matrix}  
<center><math>\begin{matrix}  
\texttt{fib}\ 4 = \texttt{y\ fibonacci}\ 4 =
  \texttt{fib}\ 4 =  
   \\= \texttt{fibonacci} (\texttt{lazy} (\texttt{y\ fibonacci}))\ 4 =
  \texttt{y\ fib\_y}\ 4  
   \\= \texttt{force} (\texttt{lazy} (\texttt{y\ fibonacci}))\ 3
   =\\=
+ \texttt{force} (\texttt{lazy} (\texttt{y\ fibonacci}))\ 2 =
  \texttt{fib\_y} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 4  
   \\= \texttt{y\ fibonacci}\ 3
   =\\=
+ \texttt{y\ fibonacci}\ 2 =
  \texttt{force} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 3  
   \\= \texttt{fibonacci} (\texttt{lazy} (\texttt{y\ fibonacci}))\ 3
  + \texttt{force} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 2  
+ \texttt{fibonacci} (\texttt{lazy} (\texttt{y\ fibonacci}))\ 2
   =\\=
   \\= \texttt{force} (\texttt{lazy} (\texttt{y\ fibonacci}))\ 2  
  \texttt{y\ fib\_y}\ 3 + \texttt{y\ fib\_y}\ 2  
+ \texttt{force} (\texttt{lazy} (\texttt{y\ fibonacci}))\ 1
   =\\=
+ 1 =  \\= \texttt{y\ fibonacci}\ 2
  \texttt{fib\_y} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 3  
+ \texttt{y\ fibonacci}\ 1
  + \texttt{fib\_y} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 2  
+ 1 =   \\= 1 + 1 + 1 = 3 \end{matrix}</math></center>
   =\\=
  \texttt{force} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 2  
  + \texttt{force} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 1 + 1
  =\\=
  \texttt{y\ fib\_y}\ 2 + \texttt{y\ fib\_y}\ 1 + 1  
  =\\=
   \texttt{fib\_y} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 2 +
  \texttt{fib\_y} (\texttt{lazy} (\texttt{y\ fib\_y}))\ 1 + 1
  =\\=
  1 + 1 + 1 = 3  
\end{matrix}</math></center>


==Laboratoria i ćwiczenia==
== Ćwiczenia ==


[[Programowanie funkcyjne/Procedury jeszcze wyższych rzędów/Ćwiczenia        |Laboratoria i ćwiczenia do wykładu]]
[[Programowanie funkcyjne/Procedury jeszcze wyższych rzędów/Ćwiczenia        |Ćwiczenia]]

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

Wstęp

Jaka jest różnica między danymi i procedurami? W programowaniu funkcyjnym to rozróżnienie rozmywa się. Dane mogą być traktowane jak procedury --- każdy interpreter czy kompilator zamienia dane (kod źródłowy programu) na procedurę (wykonywalny program). Procedury wyższych rzędów operują na innych procedurach jak na danych. Tak więc procedury mogą być również danymi. Można by powiedzieć, że dane tym różnią się od procedur, że zawsze są podmiotem obliczeń, a nigdy nie są same wykonywane. Niniejszy wykład powinien Państwa przekonać, że wcale tak nie musi być.

Używając języka programowania nie widzimy, w jaki sposób zrealizowane są struktury danych, ani jak jest zrealizowane stosowanie procedur do argumentów. Łatwo sobie wyobrazić, że procedury mogą mieć postać kodu źródłowego lub częściowo skompilowanego, który jest interpretowany. Jak zobaczymy w tym wykładzie, również dane nie muszą być biernym przedmiotem obliczeń. Poznamy jedną z możliwych implementacji danych --- całkowicie proceduralną.

Poniżej pokazujemy, jak można zaimplementować podstawowe struktury danych korzystając jedynie z procedur wyższych rzędów.

Uwaga

Niniejszy wykład ma całkowicie charakter ćwiczenia umysłowego.

Struktury danych wcale nie są implementowane w opisany sposób, ani nie jest to najlepszy sposób ich implementacji. Jednak opisany sposób ich realizacji jest możliwy.

Celem tego ćwiczenia jest zilustrowanie w pełni zamiennego traktowania danych i procedur. Zdobywszy tę umiejętność, będziemy mogli wznieść się na wyższy poziom abstrakcji i tworzyć struktury, w których dane są przemieszane z procedurami. Jest to również dobre ćwiczenie rozwijające umiejętność posługiwania się procedurami wyższych rzędów.

Pokazane tutaj konstrukcje pochodzą z λ-rachunku, gdzie faktycznie mają zastosowanie. Służą one do zaimplementowania podstawowych typów danych i operacji na nich. Robi się to w ramach dowodu, że w λ-rachunku można zapisać dowolne funkcje obliczalne.

Wartości logiczne

Aby mówić o wartościach logicznych, potrzebujemy zaimplementować następujące pojęcia i konstrukcje językowe:

  • prawdę i fałsz,
  • wyrażenia warunkowe if-then-else,
  • operacje logiczne: koniunkcję, alternatywę i negację.

Mamy dwie wartości logiczne: prawdę i fałsz. Wartość logiczną możemy reprezentować jako procedurę dwuargumentową, której wynikiem jest jeden z jej argumentów. W przypadku prawdy wybiera pierwszy, a w przypadku fałszu drugi argument.

let prawda x y = x;;
val prawda : 'a -> 'b -> 'a = <fun>

let falsz  x y = y;;
val falsz : 'a -> 'b -> 'b = <fun>

Wyrażenia warunkowe if-then-else możemy zrealizować za pomocą trójargumentowej procedury jesli. Jej pierwszym argumentem jest wartość logiczna, a dwa pozostałe argumenty to odroczone wartości. (Używamy tu odroczonych wartości bez spamiętywania, tak jak zostały one zaimplementowane w poprzednim wykładzie.) Procedura ta, w zależności od wartości warunku, wybiera jedną z odroczonych wartości i oblicza ją.

let jesli w k a = force (w k a);;
val jesli : ('a -> 'b -> unit -> 'c) -> 'a -> 'b -> 'c = <fun>

jesli prawda (lazy 1) (lazy 2);;
- : int = 1

Implementując koniunkcję i alternatywę korzystamy z następujących tożsamości:

  • prawda xx,
  • fałsz x fałsz,
  • prawda x prawda,
  • fałsz xx.

Negacja polega na zamianie rolami dwóch argumentów wartości logicznej.

let i x y  = x y falsz;;
val i : ('a -> ('b -> 'c -> 'c) -> 'd) -> 'a -> 'd = <fun>

let lub x y = x prawda y;;
val lub : (('a -> 'b -> 'a) -> 'c -> 'd) -> 'c -> 'd = <fun>

let nie x a b = x b a;;
val nie : ('a -> 'b -> 'c) -> 'b -> 'a -> 'c = <fun>

jesli (lub falsz prawda) (lazy 1) (lazy 2);;
- : int = 1

Produkty kartezjańskie

Zaimplementujemu tutaj produkty kartezjańskie dwóch typów. Produkty kartezjańskie większej liczby typów i rekordy nie różnią się istotnie.

Aby mówić o produkcie musimy mieć:

  • konstruktor pary para: 'a -> 'b -> ('a * `b),
  • rzuty na współrzędne: p1: ('a * 'b) -> 'a i p2: ('a * 'b) -> 'b.

Jak można zaimplementować parę nie używając żadnej struktury danych? Jedyna para o jakiej możemy mówić, to para argumentów procedury. Nam jednak potrzebna jest para bez procedury. Możemy jednak zdefiniować parę argumentów bez procedury -- parę argumentów czekających na procedurę, której zostaną przekazane. To bedzie nasza proceduralna implementacja pary.

let para x y = function f -> f x y;;
val para : 'a -> 'b -> ('a -> 'b -> 'c) -> 'c = <fun>

Zauważmy, że rzutowanie jest bardzo proste. Wystarczy przekazać parze procedurę, która wybierze pierwszy lub drugi z argumentów, które otrzyma. Takie procedury już znamy, są to: prawda i falsz.

let p1 p = p prawda;;
val p1 : (('a -> 'b -> 'a) -> 'c) -> 'c = <fun>

let p2 p = p falsz;;
val p2 : (('a -> 'b -> 'b) -> 'c) -> 'c = <fun>

let p x = para 1 "ala" x;;
val p : (int -> string -> 'a) -> 'a = <fun>

p1 p;;
- : int = 1

p2 p;;
- : string = "ala"
Uwaga
Tajemniczy parametr x w powyższym przykładzie jest wynikiem zastosowania eta-konwersji i ma na celu uniknięcie wprowadzenia w typie p słabych zmiennych typowych. Jeśli Czytelnik nie widzi jego celowości, nie należy się tym przejmować.

Listy

Zaimplementujemy tutaj listy nieskończone. Listy skończone można łatwo zaimplementować jako pary: długość listy + lista nieskończona.

Listy nieskończone można reprezentować jako procedury jednoargumentowe. Argumentem takiej procedury może być liczba naturalna, indeks elementu listy, a jej wynikiem jest wartość elementu listy. (To jak zaimplementować liczby naturalne pokazujemy dalej.)

Musimy zaimplementować:

  • konstruktor nieskończonej listy stałej,
  • dołączenie danego elementu na początku danej listy,
  • procedurę zwracającą pierwszy element (głowę) danej listy,
  • procedurę zwracającą ogon danej listy.

let stala x n =  x;;
val stala : 'a -> 'b -> 'a = <fun>

let sklej h t n = if n = 0 then h else t (n-1);; 
val sklej : 'a -> (int -> 'a) -> int -> 'a = <fun>

let glowa l = l 0;;
val glowa : (int -> 'a) -> 'a = <fun>

let ogon l n = l (n + 1);;
val ogon : (int -> 'a) -> int -> 'a = <fun>

Liczby naturalne Churcha

Pozostały nam jeszcze liczby naturalne. (Rozszerzenie liczb naturalnych do całkowitych pozostawiamy jako ćwiczenie dla ambitnych czytelników. :-) Będziemy utożsamiać liczbę naturalną z liczbą iteracji zadanej funkcji. Dokładniej, liczbę n implementujemy jako procedurę, której argumentem jest funkcja (nazwijmy ją f), a wynikiem fn.

n  ffn

Używając tej analogii, zero reprezentujemy jako funkcję zwracającą identyczność, a jedynkę jako identyczność (na funkcjach).

let id x = x;;
val id : 'a -> 'a = <fun>

let compose f g = function x -> f (g x);;
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>

let zero f = id;;
val zero : 'a -> 'b -> 'b = <fun>

let jeden f x = f x;;
val jeden : ('a -> 'b) -> 'a -> 'b = <fun>


Dodawanie i mnożenie definiujemy korzystając z poniższych toższamości:


fn+1=fnf

let inc n f = compose (n f) f;;
val inc : (('a -> 'b) -> 'b -> 'c) -> ('a -> 'b) -> 'a -> 'c = <fun>


fm+n=fmfn

let plus m n f = compose (m f) (n f);;
val plus : ('a -> 'b -> 'c) -> ('a -> 'd -> 'b) -> 'a -> 'd -> 'c = <fun>


fmn=(fn)m=m(fn)=m(n f)=(mn) f

let razy = compose;;


Do celów testowych możemy używać następujących procedur konwertujących liczby proceduralne na int i odwrotnie:

let ile n = n (function x -> x + 1) 0;;
val ile : ((int -> int) -> int -> 'a) -> 'a = <fun>

let rec iterate n f =
  if n = 0 then id else compose (iterate (n-1) f) f;;
val iterate : int -> ('a -> 'a) -> 'a -> 'a = <fun>

ile (iterate 1000);;
- : int = 1000
 
let dwa x = plus jeden jeden x;;
val dwa : ('a -> 'a) -> 'a -> 'a = <fun>

ile (razy dwa dwa);;
- : int = 4      


Jak moglibyśmy odróżniać od siebie liczby naturalne Churcha? Podstawową operacją porównania jest tzw. test zera --- sprawdzenie, czy dana liczba jest równa zero. Szukamy więc takiej funkcji f, że f0=id jest czymś istotnie innym, niż fi dla i>0. Taką funkcją może być n.p.: f(x)=falsz. Wówczas f0 prawda = prawda, natomiast fi prawda = falsz (dla i>0).

let test_zera x = x (function _ -> falsz) prawda;;
val test_zera : (('a -> 'b -> 'c -> 'c) -> ('d -> 'e -> 'd) -> 'f) -> 'f = <fun>

jesli (test_zera zero) (lazy 1) (lazy 2);;
- : int = 1      


Jak zmniejszyć daną liczbę n o 1? Liczba n to taki obiekt, który przekształca zadaną funkcję f w funkcję fn. Nie mamy jednak żadnego dostępu do tego jak n to robi. Problem ten można przedstawić sobie tak: mając dane n, f i x należy obliczyć fn1(x).

Rozważmy funkcję:

g(a,b)=(b,f b)

oraz ciąg:

(gi(x,x))i=0,,n=((x,x),(x,f x),(f x,f2 x),,(fn1 x,fn x))

Wystarczy więc z ostatniej pary w ciągu, gn(x,x)=(fn1 x,fn x) wydobyć pierwszą współrzędną. Można to zapisać tak:

let dec n f x = 
  let g (a, b) = (b, f b)
  in let (y, _) = n g (x, x) 
     in y;;
val dec:(('a*'b->'b*'c)->'d*'d->'e*'f)->('b->'c)->'d->'e      

lub tak, wykorzystując zaimplementowane przez nas produkty kartezjańskie:

let dec n f x = p1 (n (function p -> para (p2 p) (f (p2 p))) (para x x));;
val dec : (((('a -> 'b -> 'b) -> 'c) -> ('c -> 'd -> 'e) -> 'e) -> (('f -> 'f -> 'g) -> 'g) -> ('h -> 'i -> 'h) -> 'j) -> ('c -> 'd) -> 'f -> 'j = <fun>
 
ile (dec dwa);;
- : int = 1      

Odejmowanie to wielokrotne zmniejszanie o jeden:

let minus m n = (n dec) m;;
val minus : 'a -> (((('b * 'c -> 'c * 'd) -> 'e * 'e -> 'f * 'g) -> ('c -> 'd) -> 'e -> 'f) -> 'a -> 'h) -> 'h = <fun>

ile (minus dwa jeden);;
- : int = 1      

Zwróćmy uwagę, że nie mamy liczb ujemnych, a w wyniku odjęcia od mniejszej liczby większej otrzymujemy zawsze zero. Za pomocą odejmowania i testu zera można zaimplementować porównanie:

let wieksze m n = nie (test_zera (minus m n));;
val wieksze : 'a -> (((('b * 'c -> 'c * 'd) -> 'e * 'e -> 'f * 'g) -> ('c -> 'd) -> 'e -> 'f) -> 'a -> ('h -> 'i -> 'j -> 'j) -> ('k -> 'l -> 'k) -> 'm -> 'n -> 'o) -> 'n -> 'm -> 'o = <fun>

jesli (wieksze dwa jeden) (lazy 1) (lazy 2);;
- : int = 1

Abstrakcja rekurencji

Zastanowimy się teraz nad następującym problemem. Procedury fold_left i fold_right stanowią abstrakcję rekurencyjnego przetwarzanialist. Czy istnieje abstrakcja wszelkiej rekurencji? Nasze zadanie możemy sformułować tak: czy istnieje jedna konstrukcja, za pomocą której możemy zdefiniować dowolne procedury rekurencyjne, bez (dalszego) używania rekurencji? Odpowiedź brzmi: tak.

Jak można przerobić rekurencyjną definicję procedury na nierekurencyjną --- poprzez wprowadzenie dodatkowego parametru funkcyjnego. Zobaczmy to na przykładzie procedury obliczającej silnię. Procedura rekurencyjna ma postać:

let rec factorial n = 
  if n <= 1 then 
    1 
  else
    n * factorial (n - 1);;
val factorial : int -> int = <fun>

Jeżeli usuniemy słowo kluczowe rec, to wewnątrz definicji nie będziemy mogli wywoływać procedury factorial. Możemy jednak dodać dodatkowy parametr proceduralny fac i zamiast wywołania rekurencyjnego wywoływać procedurę przekazywaną przez ten parametr.

let factorial fac n = 
  if n <= 1 then 
    1 
  else 
    n * fac (n - 1);;
val factorial : (int -> int) -> int -> int = <fun>

Przyjrzyjmy się bliżej tak zdefiniowanej procedurze. Można powiedzieć, że przetwarza ona procedurę fac, która poprawnie liczy silnię dla liczb 0,1,,n na procedurę, która poprawnie liczy silnię dla liczb 0,1,,n+1. Jeżeli natomiast fac byłoby procedurą poprawnie liczącą silnię dla wszystkich liczb naturalnych, to factorial fac również byłoby taką procedurą. Tak więc chcemy skonstruować procedurę obliczającą funkcję, która byłaby punktem stałym przekształcenia factorial. Będzie ona poprawnie liczyć silnię dla wszystkich liczb.

Szukamy więc czegoś, co się nazywa operatorem punktu stałego dla procedur przekształcających procedury w procedury. Czytelnicy zaznajomieni z Semantyką i weryfikacją programów zapewne zauważyli, że szukamy (w odpowiedniej dziedzinie) kresu górnego ciągu:

(function n -> )
factorial (function n -> )
factorial (factorial (function n -> ))
factorial (factorial (factorial (function n -> )))
    

Załóżmy, że f jest procedurą przekształcającą procedurę w procedurę (takiego samego typu). W pierwszym podejściu, moglibyśmy spróbować zdefiniować operator punktu stałego następująco:

let rec y f = f (y f);;
val y : ('a -> 'a) -> 'a = <fun>
 
let f = y factorial ;;
= factorial (y factorial) = factorial (factorial (y factorial)) = ...      


Jednak ze względu na "gorliwe" obliczanie arguemntów procedur w Ocamlu, argument procedury factorial jest obliczany przed jej zastosowaniem, co powoduje zapętlenie obliczenia. Aby uniknąć zapętlenia, należy uleniwić ten argument i wyliczać go tylko na żądanie. Oto drugie podejście do implementacji operatora punktu stałego:

let factorial fac n = 
  if n <= 1 then 
    1 
  else 
    n * (force fac (n - 1));;
val factorial : (int -> int) Lazy.t -> int -> int = <fun>
 
let rec y f = f (lazy (y f));;
val y : ('a lazy_t -> 'a) -> 'a = <fun>

let fac = y factorial ;;
val fac : int -> int = <fun>

fac 3=(y factorial) 3==(factorial(lazy(y factorial))) 3==3*(force(lazy(y factorial)) 2)==3*(y factorial 2)==3*(factorial(lazy(y factorial)) 2)==3*(2*(force(lazy(y factorial))) 1)==3*(2*(y factorial 1))==3*(2*(factorial(lazy(y factorial)) 1))==3*(2*1)=6

Operator punktu stałego może być zastosowany nie tylko do obliczania silni. Za jego pomocą można każdą procedurę rekurencyjną zdefiniować bez używania rekurencji. Powiedzmy, że nasza procedura rekurencyjna nazywa się p. Należy w tym celu:

  • zmienić nazwę definiowanej procedury, powiedzmy na p_y,
  • dodać jej dodatkowy (pierwszy) parametr, nazwijmy go f,
  • każde odwołanie rekurencyjne do procedury p zastąpić przez force f,
  • zdefinować p jako y p_y.

Oto przykład zastosowania tej techniki do rekurencyjnej procedury obliczającej liczby Fibonacciego:

let fib_y f n = 
  if n <= 2 then 
    1
  else'
    (force f (n-1)) + (force f (n-2));;

let fib = y fib_y;;
fib 4=y fib_y 4==fib_y(lazy(y fib_y)) 4==force(lazy(y fib_y)) 3+force(lazy(y fib_y)) 2==y fib_y 3+y fib_y 2==fib_y(lazy(y fib_y)) 3+fib_y(lazy(y fib_y)) 2==force(lazy(y fib_y)) 2+force(lazy(y fib_y)) 1+1==y fib_y 2+y fib_y 1+1==fib_y(lazy(y fib_y)) 2+fib_y(lazy(y fib_y)) 1+1==1+1+1=3

Ćwiczenia

Ćwiczenia