Programowanie funkcyjne/Strumienie: 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 65 wersji utworzonych przez 3 użytkowników)
Linia 1: Linia 1:
==Czas rzeczywisty, a czas symulowany==
== Wstęp ==
<p align="justify">
Przedstawiając konstrukcje imperatywne, powiedzieliśmy, że w programowaniu imperatywnym upływ czasu w świecie rzeczywistym jest modelowany przez upływ czasu w programie komputerowym.
W "czystym" programowaniu funkcyjnym nie ma zmiennych, ani zmian stanów.
Trudno też mówić o upływie czasu w programie.
Wszak program składa się z implementacji pojęć matematycznych.
Jedyna zależność związana z czasem polega na tym, że pojęcia wykorzystywane muszą być zdefiniowane ''przed'' pojęciami korzystającymi z nich.
</p>


Programowanie imperatywne i analogia obiektowa. Czas w modelowanym systemie jest modelowany przez czas w modelu systemu.
<p align="justify">
Strumienie to po prostu ciągi wartości, być może nieskończone.
Programowanie strumieniowe pozwala jednak na utworzenie związku między czasem i obliczeniami.  
</p>


Programowanie strumieniowe --- nie ma takiej analogii z czasem. Staramy się w modelu oddać strukturę zależności, a niekoniecznie kolejność zdarzeń.
<p align="justify">
W fizyce mamy pojęcie czasoprzestrzeni.
Sama czasoprzestrzeń nie zmienia się w czasie -- jest to twór matematyczny ujmujący upływ czasu i historię całej przestrzeni.  
Historia pojedynczej cząstki jest reprezentowana przez jej trajektorię w czasoprzestrzeni,  
nazywaną ''linią świata'' cząstki.  
</p>


Strumień, to ciąg wartości --- cała, być może nieskończona, historia obiektu. Zamiast mówić o akcjach i zmianach stanu obiektów w danych chwilach patrzymy "jednym rzutem oka" na całą historię obiektu w czasoprzestrzeni. Staramy się oddać zależności między liniami świata jednych obiektów i innych.
<p align="justify">
Jak zobaczymy, strumienie mogą być użyte do przedstawiania historii obliczeń iteracyjnych.
Strumień może reprezentować całą, być może nieskończoną historię pewnych obliczeń.  
Pozwala to "jednym rzutem oka" spojrzeć na całą historię obliczenia.
Jak zobaczymy, pozwoli to również na implementowanie pewnych ciekawych operacji na
"liniach świata obliczeń".
</p>
 
<p align="justify">
Wiele z podanych w tym wykładzie przykładów pochodzi z [[Programowanie_funkcyjne#Literatura | [AS]]], przy czym zostały przeniesione z języka Scheme do Ocamla.
</p>
 
==Definiowanie form specjalnych==
<p align="justify">
Jak już pisaliśmy wcześniej, 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. instrukcja warunkowa <tt>if-then-else</tt> ---
w zależności od wartości warunku, tylko jeden z pozostałych członów jest obliczany.
</p>
 
<p align="justify">
Czy można takie formy specjalne zaimplementować samemu?
Tak.
Potrzeba do tego dwóch rzeczy.
Musimy umieć odroczyć moment obliczania wartości i wprowadzać własne ''makrodefinicje''.
</p>
 
<p align="justify">
O uleniwianiu i odraczaniu obliczeń mówiliśmy w poprzednim wykładzie.
W Ocamlu możemy nie tylko definiować własne makrodefinicje, ale wręcz zmieniać składnię języka (sic!).
Za analizę składniową odpowiedzialny jest preprocesor P4. 
Nie będziemy tu mówić o nim, gdyż wykracza to poza zakres tego kursu.
Jednak raz użyjemy definicji wprowadzającej prostą makrodefinicję, która realizuje formę specjalną.
</p>


==Implementacja strumieni==
==Implementacja strumieni==
<p align="justify">
Formalnie strumień to ciąg elementów --- być może nieskończony.
Pomysł polega jednak na tym, żeby był on "leniwy", tzn. obliczane były tylko potrzebne wartości.
Przyjmujemy przy tym, że wartość stają się potrzebne zgodnie z ich kolejnością występowania w strumieniu. 
Tak więc w każdej chwili strumień składa się ze skończonego ciągu obliczonych (potrzebnych) wartości oraz odroczonego (być może nieskończonego) ciągu pozostałych elementów.
Implementując strumienie korzystamy z uleniwiania (zaimplementowanego przez moduł <tt>Lazy</tt>).
Strumień, jeśli nie jest pusty, to para: wartość pierwszego elementu i odroczony strumień pozostałych wartości. 
</p>


Formalnie strumień to ciąg --- być może nieskończony. Pomysł polega jednak na tym, żeby był on "leniwy", tzn. obliczane były tylko potrzebne wartości. Tak więc strumień to para: wartość i odroczony strumień pozostałych wartości.
  '''type''' 'a stream = Nil | Cons '''of''' 'a * 'a stream Lazy.t;;
     
''type 'a stream = Nil | Cons of 'a * 'a stream Lazy.t''
  '''type''' 'a stream = Nil | Cons '''of''' 'a Lazy.t * 'a stream Lazy.t;;
   
  &nbsp;&nbsp;
  '''let''' empty s = s = Nil;;
  '''let''' empty s = s = Nil;;
  &nbsp;&nbsp;
  ''val empty : 'a stream -> bool = <fun>''
 
<p align="justify">
Podstawowe "cegiełki" do budowy strumieni są dokładnie takie same jak do budowy list:
pusty strumień oraz dołączenie elementu na początku strumienia.
Dwie podstawowe operacje służące do dekonstrukcji strumieni, to procedury zwracające głowę i ogon danego strumienia.
</p>
 
  '''let''' head s =  
  '''let''' head s =  
&nbsp;&nbsp;'''match''' s '''with'''
  '''match''' s '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;Nil -> failwith "Empty" |
    Nil -> failwith "Empty" |
&nbsp;&nbsp;&nbsp;&nbsp;Cons (h, _) -> force h;;
    Cons (h, _) -> h;;
  &nbsp;&nbsp;
  ''val head : 'a stream -> 'a = <fun>''
  '''let''' tail s =  
  '''let''' tail s =  
&nbsp;&nbsp;'''match''' s '''with'''
  '''match''' s '''with'''
&nbsp;&nbsp;&nbsp;&nbsp;Nil -> failwith "Empty" |
    Nil -> failwith "Empty" |
&nbsp;&nbsp;&nbsp;&nbsp;Cons (_, t) -> force t;;  
    Cons (_, t) -> Lazy.force t;;
  &nbsp;&nbsp;
  ''val tail : 'a stream -> 'a stream = <fun>''
EXTEND ... Makro Cons
 
     
<p align="justify">
<tt>Cons</tt> zostaje zdefiniowane jako makro:
Tak zdefiniowany typ danych jest trochę niewygodny w użyciu, gdyż zawsze gdy używamy <tt>Cons</tt>,
powinniśmy go używać razem z <tt>lazy</tt>.
Aby ułatwić sobie życie, zdefiniujemy <tt>Cons</tt> jako makro, które przed drugim swoim parametrem dodaje słowo kluczowe <tt>lazy</tt>.
</p>


<center><math>
<center><math>
\texttt{Cons}(x, y) \equiv  
\texttt{Cons}(x, y) \equiv  
\texttt{Cons}(\texttt{lazy}\ x, \texttt{lazy}\ y)
\texttt{Cons}(x, \texttt{lazy}\ y)
</math></center>
</math></center>


<p align="justify">
Oto niezbędne "zaklęcie", które podajemy bez wyjaśnień.
Zainteresowanych odsyłamy do dokumentacji preprocesora P4.
</p>
#load "camlp4o.cma";;
#load "pa_extend.cmo";;
#load "q_MLast.cmo";;
'''EXTEND'''
  Pcaml.expr: '''LEVEL''' "simple"
  [ [ UIDENT "Cons"; param=SELF ->
        '''let''' h,l =
          '''match param with <:expr< ($list:[h;l]$) >> -> (h,l)
                            | _ -> raise Not_found
        in
          <:expr< Cons ($h$) (lazy $l$)>> ] ]
  ;
'''END''';;
=== Operacje na strumieniach ===
<p align="justify">
Mając do dyspozycji takie konstruktory i selektory możemy zdefiniować kilka pomocniczych operacji na strumieniach:
Mając do dyspozycji takie konstruktory i selektory możemy zdefiniować kilka pomocniczych operacji na strumieniach:
</p>
        
        
  '''let''' '''rec''' const_stream c =  
  '''let''' '''rec''' const_stream c =  
&nbsp;&nbsp;Cons (c, const_stream c);;  
  Cons (c, const_stream c);;  
  &nbsp;&nbsp;
  ''val const_stream : 'a -> 'a stream = <fun>''
Definiuje nieskończony strumień, którego wszystkie elementy są równe <tt>c</tt>.
 
 
  '''let''' ones = const_stream 1;;
  '''let''' ones = const_stream 1;;
  &nbsp;&nbsp;
  ''val ones : int stream = Cons (1, <lazy>)''
To jest nieskończony strumień jedynek.
 
 
  let '''rec''' filter_stream p s =  
  let '''rec''' filter_stream p s =  
&nbsp;&nbsp;'''if''' empty s '''then'''  
  '''if''' empty s '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;Nil
    Nil
&nbsp;&nbsp;'''else'''  
  '''else'''  
&nbsp;&nbsp;&nbsp;&nbsp;'''let''' h = head s  
    '''let''' h = head s  
&nbsp;&nbsp;&nbsp;&nbsp;'''and''' t = tail s
    '''and''' t = tail s
&nbsp;&nbsp;&nbsp;&nbsp;'''in'''  
    '''in'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' p h '''then'''  
      '''if''' p h '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (h, filter_stream p t)
        Cons (h, filter_stream p t)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else'''
      '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;filter_stream p t;;
        filter_stream p t;;
  &nbsp;&nbsp;
  ''val filter_stream : ('a -> bool) -> 'a stream -> 'a stream = <fun>''
<p align="justify">
Działa podobnie jak procedura <tt>filter</tt> dla list, tworzy strumień złożony wyłącznie z tych elementów danego strumienia, które spełniają podany predykat.
Jeśli strumień będący parametrem jest nieskończony, to nieskończenie wiele jego elementów powinno spełniać predykat,
tak żeby strumień wynikowy był również nieskończony.
</p>
 
 
  '''let''' '''rec''' stream_ref s n =  
  '''let''' '''rec''' stream_ref s n =  
&nbsp;&nbsp;'''if''' n = 0 '''then'''  
  '''if''' n = 0 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;head s
    head s
&nbsp;&nbsp;'''else'''
  '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;stream_ref (tail s) (n - 1);;
    stream_ref (tail s) (n - 1);;
  &nbsp;&nbsp;
  ''val stream_ref : 'a stream -> int -> 'a = <fun>''
Zwraca element strumienia z podanej pozycji. Pozycje w strumieniu są indeksowane od 0.
 
 
  '''let''' '''rec''' stream_map f s =  
  '''let''' '''rec''' stream_map f s =  
&nbsp;&nbsp;'''if''' empty s '''then'''  
  '''if''' empty s '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;Nil
    Nil
  &nbsp;&nbsp;'''else'''
  '''else'''
  &nbsp;&nbsp;&nbsp;&nbsp;Cons (f (head s), stream_map f (tail s));;
    Cons (f (head s), stream_map f (tail s));;
  ''val stream_map : ('a -> 'b) -> 'a stream -> 'b stream = <fun>''
<p align="justify">
Działa podobnie jak procedura <tt>map</tt> dla list,
stosuje dane przekształcenie <tt>f</tt> do wszystkich elementów danego strumienia i tworzy strumień wyników.
</p>
 
  '''let''' scale_stream c s =
  stream_map (function x -> x *. c) s;;
''val scale_stream : float -> float stream -> float stream = <fun>''
<p align="justify">
Procedura ta mnoży elementy danego strumienia przez zadaną stałą.
</p>
 
'''let rec''' stream_map2 f s1 s2 =
  '''if''' empty s1 or empty s2 '''then''' Nil
  '''else''' Cons (f (head s1) (head s2), stream_map2 f (tail s1) (tail s2));;
''val stream_map2 : ('a -> 'b -> 'c) -> 'a stream -> 'b stream -> 'c stream = <fun>''
 
<p align="justify">
Jest to odpowiednik procedury <tt>map2</tt> z modułu <tt>List</tt>.
Pozwala ona na proste zdefiniowanie wielu operacji łączących strumienie.
Na przykład, dodawanie strumieni:
</p>
 
'''let''' '''rec''' add_int_streams s1 s2 = stream_map2 (+) s1 s2;;
''val add_int_streams : int stream -> int stream -> int stream = <fun>''
 
 
'''let''' sample s =
  '''let rec''' pom i s =
    '''if''' i = 0 || empty s '''then''' []
    '''else''' head s :: pom (i-1) (tail s)
  '''in'''
    pom 12 s;;
''val sample : 'a stream -> 'a list = <fun>''
Procedury tej będziemy używać do testowania konstruowanych przez nas strumieni.
sample ones;;
''- : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1]''
 
{{uwaga||uwaga_Stream|
W tym wykladzie pokazujemy, jak zaimplementować strumienie, choć Ocaml zawiera moduł (<tt>Streams</tt>) implementujący strumienie. Jednak są to dwie różne struktury danych. Strumienie zaimplementowane w module <tt>Streams</tt> odpowiadają bardziej pojęciu strumienia w C++. }}


==Przykłady strumieni nieskończonych==
==Przykłady strumieni nieskończonych==
<p align="justify">
Zdefiniujmy strumień liczb naturalnych:
</p>


Spróbujmy zdefiniować strumień liczb naturalnych:
     
  '''let''' '''rec''' integers_from x =  
  '''let''' '''rec''' integers_from x =  
&nbsp;&nbsp;Cons(x, integers_from (x+1));;
  Cons(x, integers_from (x+1));;
  &nbsp;&nbsp;
  ''val integers_from : int -> int stream = <fun>''
  '''let''' nats = integers_from 0;;
  '''let''' nats = integers_from 0;;
     
''val nats : int stream = Cons (0, <lazy>)''
sample nats;;
''- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11]''
 
Podobnie możemy zdefiniować strumień liczb Fibonacciego:
Podobnie możemy zdefiniować strumień liczb Fibonacciego:
     
 
  '''let''' fibs =  
  '''let''' fibs =  
&nbsp;&nbsp;'''let''' '''rec''' fibo a b =  
  '''let''' '''rec''' fibo a b =  
&nbsp;&nbsp;&nbsp;&nbsp;Cons (a, fibo b (a+b))
    Cons (a, fibo b (a+b))
&nbsp;&nbsp;'''in''' fibo 0 1;;
  '''in''' fibo 0 1;;
     
''val fibs : int stream = Cons (0, <lazy>)''
Jak widać, jeżeli jesteśmy w stanie skonstruować iteracyjną procedurę wyliczającą kolejne elementy strumienia, to tym samym jesteśmy w stanie zdefiniować strumień tych wartości. Procedura taka stanowi ukrytą w strumieniu "maszynerię", która na żądanie podaje kolejne elementy strumienia. Takie definicje strumieni nazywamy ''nie uwikłanymi''(w odróżnieniu od ''uwikłanych'', które poznamy dalej).W przypadku definicji nie uwikłanych bytem rekurencyjnym jest procedura, a nie strumień. Strumień jest wtórny wobec procedury rekurencyjnej.
sample fibs;;
''- : int list = [0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89]''
 
<p align="justify">   
Jak widać, jeżeli jesteśmy w stanie skonstruować iteracyjną procedurę wyliczającą kolejne elementy strumienia, to tym samym jesteśmy w stanie zdefiniować strumień tych wartości.  
Procedura taka stanowi ukrytą w strumieniu "maszynerię", która na żądanie podaje kolejne elementy strumienia. Takie definicje strumieni nazywamy ''nie uwikłanymi'' (w odróżnieniu od ''uwikłanych'', które poznamy dalej).
Charakterystyczne dla definicji nie uwikłanych jest to, że bytem definiowanym rekurencyjnie jest procedura,  
a nie strumień. Strumień jest zdefiniowany dopiero w oparciu o rekurencyjnie zdefiniowaną procedurę.
</p>
 
<p align="justify">
Strumienie to tylko jeden ze sposobów konstruowania złożonych struktur danych.
Można więc go łączyć z dowolnymi innymi sposobami konstruowania danych i tworzyć strumienie list,
listy strumieni, czy nawet strumienie strumieni.  
Oto przykład, strumień strumieni powstałych z danego (nieskończonego) strumienia przez usunięcie coraz większej liczby początkowych elementów (poczynając od zera elementów):
</p>


Strumienie to tylko jeden ze sposobów konstruowania złożonych struktur danych, taki jak np. listy. Można więc tworzyć strumienie list, listy strumieni, czy strumienie strumieni.  Oto przykład:
     
  '''let''' '''rec''' tails s =  
  '''let''' '''rec''' tails s =  
&nbsp;&nbsp;Cons (s, tails (tail s));;  
  '''if''' empty s '''then''' Cons (s, Nil) '''else''' Cons (s, tails (tail s));;
     
''val tails : 'a stream -> 'a stream stream = <fun>''
==Strumień liczb pierwszych - sito Eratostenesa==
 
=== Strumień liczb pierwszych -- sito Eratostenesa ===
<p align="justify">
Spróbujmy skonstruować nieskończony strumień liczb pierwszych, generowanych metodą sita Eratostenesa.
Sposób konstrukcji takiego strumienia możemy przedstawić w postaci schematu przypominającego schematy blokowe układów przetwarzających sygnały.
Strumienie są na nim zaznaczone liniami ciągłymi, a pojedyncze wartości przerywanymi.
</p>


Spróbujmy skonstruować nieskończony strumień liczb pierwszych, generowanych metodą sita Eratostenesa. Sposób konstrukcji takiego  strumienia możemy przedstawić w postaci schematu przypominającego schematy blokowe układów przetwarzających sygnały. Strumienie są zaznaczone liniami ciągłymi, a pojedyncze wartości przerywanymi.  
<p align="justify">
Poniższy diagram przedstawia budowę procedury <tt>sito</tt> implementującej pojedyncze sito.
Zauważmy, że jest on rekurencyjny, tzn. procedura ta pojawia się również na tym schemacie.  
Odpowiada to temu, że liczby przesiane przez jedno sito trafiają na kolejene sito, itd.
</p>


TU GRAFIKA
[[grafika:pf-rys-10-1.png||center]]


Schemat ten możemy zapisać w postaci następującego programu:
Schemat ten przekłada się na następujący program:
        
        
  '''let''' sitko p s =  
  '''let''' sitko p s =  
&nbsp;&nbsp;filter_stream ('''function''' x -> (x mod p) <> 0) s;;
  filter_stream ('''function''' x -> (x mod p) <> 0) s;;
  &nbsp;&nbsp;
  ''val sitko : int -> int stream -> int stream = <fun>''
  '''let''' '''rec''' sito s  =  
  '''let''' '''rec''' sito s  =  
&nbsp;&nbsp;Cons (head s, sito (sitko (head s) (tail s)));;
  Cons (head s, sito (sitko (head s) (tail s)));;
  &nbsp;&nbsp;
  ''val sito : int stream -> int stream = <fun>''
 
  '''let''' primes =  
  '''let''' primes =  
&nbsp;&nbsp;sito (integers_from 2);;
  sito (integers_from 2);;
''val primes : int stream = Cons (2, <lazy>)''
sample primes;;
''- : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37]''
 
<p align="justify">
Dzięki leniwości strumieni, w zależności od tego ile liczb odczytamy ze strumienia,
powstanie odpowiednia liczba sit i odpowiednia ilość liczb zostanie przez nie przesiana.
</p>


==Definicje uwikłane==
==Definicje uwikłane==
 
<p align="justify">
Definiując nieskończone strumienie nie musimy tego zawsze robić poprzez podanie odpowiedniej procedury rekurencyjnej (<tt>integers-from</tt>, <tt>fibgen</tt>, <tt>sieve</tt>).Możemy użyć elementów strumienia do zdefiniowania jego samego.
Definiując nieskończone strumienie nie musimy tego zawsze robić poprzez podanie odpowiedniej procedury rekurencyjnej (por. <tt>integers-from</tt>, <tt>fibo</tt>, <tt>sieve</tt>).
Możemy rekurencyjnie użyć strumienia do zdefiniowania jego samego.
</p>
        
        
  '''let''' '''rec''' ones =
  '''let''' '''rec''' ones =
&nbsp;&nbsp;Cons (1, ones);;
  Cons (1, ones);;
     
Do budowy bardziej skomplikowanych strumieni potrzebujemy dodatkowych operacji budujących strumienie. Na przykład, do utworzenia strumienia liczb naturalnych czy liczb Fibonacciego potrzebujemy dodawania strumieni.
sample ones;;
     
''- : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1]''
'''let''' '''rec''' add_int_streams s1 s2 =
 
&nbsp;&nbsp;'''if''' empty s1 '''or''' empty s2 '''then'''
<p align="justify">
&nbsp;&nbsp;&nbsp;&nbsp;Nil
Do budowy bardziej skomplikowanych strumieni potrzebujemy dodatkowych operacji tworzących strumienie.  
&nbsp;&nbsp;'''else'''
Na przykład, do utworzenia strumienia liczb naturalnych czy liczb Fibonacciego potrzebujemy dodawania strumieni.
&nbsp;&nbsp;&nbsp;&nbsp;Cons (head s1 + head s2, add_int_streams (tail s1) (tail s2));;
</p>
&nbsp;&nbsp;
 
  '''let''' '''rec''' nats =
  '''let''' '''rec''' nats =
&nbsp;&nbsp;Cons (0, add_int_streams ones nats);;
  Cons (0, add_int_streams ones nats);;
     
''val nats : int stream = Cons (0, <lazy>)''
sample nats;;
''- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11]''
 
<p align="justify">
Sposób obliczania kolejnych elementów strumieni zdefiniowanych w sposób uwikłany najlepiej przedstawić sobie w postaci tabelki:
Sposób obliczania kolejnych elementów strumieni zdefiniowanych w sposób uwikłany najlepiej przedstawić sobie w postaci tabelki:
</p>


 
<p>
<center>
<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250">
<math>
<param name="DIR" value="images/tab1/">
\begin{matrix}
</applet>
\texttt{ones} &  & 1 & 1 & 1 & 1 & 1 & 1 & 1 & \dots \\
</p>
\texttt{nats} &  & 0 & 1 & 2 & 3 & 4 & 5 & 6 & \dots \\
\texttt{nats} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & \dots
\end{matrix}
</math>
</center>
 


Zamiast dodawania strumienia jedynek, możemy użyć operacji zwiększania o jeden:
Zamiast dodawania strumienia jedynek, możemy użyć operacji zwiększania o jeden:
        
        
  '''let''' succ x = x + 1;;
  '''let''' succ x = x + 1;;
''val succ : int -> int = <fun>''
  '''let''' '''rec''' nats =  
  '''let''' '''rec''' nats =  
&nbsp;&nbsp;Cons(0, stream_map succ nats);;
  Cons(0, stream_map succ nats);;
     
''val nats : int stream = Cons (0, <lazy>)''
<center>
<math>
sample nats;;
\begin{matrix}
''- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11]''
\texttt{nats} &  & 0 & 1 & 2 & 3 & 4 & 5 & 6 & \dots \\
 
\texttt{nats} & 0 & 1 & 2 & 3 & 4 & 5 & 6 & 7 & \dots
<p>
\end{matrix}
<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250">
</math>
<param name="DIR" value="images/tab2/">
</center>
</applet>
     
</p>
 
<p align="justify">
A oto uwikłana definicja liczb Fibonacciego:
</p>


  '''let''' '''rec''' fibs =  
  '''let''' '''rec''' fibs =  
&nbsp;&nbsp;Cons (0, Cons (1, add_int_streams fibs (tail fibs)));;
  Cons (0, Cons (1, add_int_streams fibs (tail fibs)));;
&nbsp;&nbsp;
  ''val fibs : int stream = Cons (0, <lazy>)''
  '''let''' tails s =
   
  &nbsp;&nbsp;'''let''' '''rec''' w =  
sample fibs;;
&nbsp;&nbsp;&nbsp;&nbsp;Cons (s, stream_map tail w)
''- : int list = [0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89]''
&nbsp;&nbsp;'''in''' w;;


<center>      
<p>
<math>
<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250">
\begin{matrix}
<param name="DIR" value="images/tab3/">
\texttt{fibs}      &  &  & 0 & 1 & 1 & 2 & 3 & 5 & \dots \\
</applet>
\texttt{tail\ fibs} &  &  & 1 & 1 & 2 & 3 & 5 & 8 & \dots \\
</p>
\texttt{fibs}      & 0 & 1 & 1 & 2 & 3 & 5 & 8 & 13 & \dots \\
\end{matrix}
</math>
</center>


<p align="justify">
Możemy też w uwikłany sposób zdefiniować strumień liczb pierwszych.
Użyjemy do tego predykatu <tt>prime</tt> sprawdzającego, czy liczba jest pierwsza ...
a predykat <tt>prime</tt> zdefiniujemy używając strumienia liczb pierwszych.
</p>


Możemy też w uwikłany sposób zdefiniować strumień liczb pierwszych. Użyjemy do tego predykatu sprawdzającego, czy liczba jest pierwsza:
'''let''' divisible x y = x mod y = 0;;
     
''val divisible : int -> int -> bool = <fun>''
let square x = x * x;;
''val square : int -> int = <fun>''
  '''let''' '''rec''' primes =  
  '''let''' '''rec''' primes =  
&nbsp;&nbsp;Cons (2, filter_stream prime (integers_from 3))
  Cons (2, filter_stream prime (integers_from 3))
     
Natomiast predykat <tt>prime</tt> zdefiniujemy używając ... strumienia liczb pierwszych:
     
  '''and''' prime n =  
  '''and''' prime n =  
&nbsp;&nbsp;'''let''' '''rec''' iter ps =  
  '''let''' '''rec''' iter ps =  
&nbsp;&nbsp;&nbsp;&nbsp;'''if''' square (head ps) > n '''then'''  
    '''if''' head ps * head ps > n '''then''' true
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;true
    '''else''' '''if''' divisible n (head ps) '''then''' false
&nbsp;&nbsp;&nbsp;&nbsp;'''else''' '''if''' divisible n (head ps) '''then''' false
    '''else''' iter (tail ps)
&nbsp;&nbsp;&nbsp;&nbsp;'''else''' iter (tail ps)
  '''in'''
&nbsp;&nbsp;'''in'''  
    iter primes;;
  &nbsp;&nbsp;&nbsp;&nbsp;iter primes;;
''val primes : int stream = Cons (2, <lazy>)''
     
''val prime : int -> bool = <fun>''
Całość działa poprawnie, ponieważ strumień jest leniwą strukturą danych. Pierwszy element strumienia liczb pierwszych, 2, jest dany
   
explicite. Natomiast sprawdzając kolejne liczby, czy są pierwsze, zawsze mamy już obliczone potrzebne liczby pierwsze.
sample primes;;
''- : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37]''


Konkretnie, największa obliczona liczba pierwsza <math>p</math> wystarcza do przetestowania pierwszości liczb do <math>p^2</math> włącznie. Natomiast kolejna liczba pierwsza <math>p'</math> spełnia <math>p' < 2p \le p^2</math>.
<p align="justify">
Całość działa poprawnie, ponieważ strumień jest leniwą strukturą danych.
Pierwszy element strumienia liczb pierwszych, 2, jest dany explicite.  
Natomiast sprawdzając czy kolejne liczby są pierwsze, zawsze mamy już obliczone potrzebne do tego liczby pierwsze.
</p>


<p align="justify">
Konkretnie, podana explicite pierwsza liczba pierwsza, 2, pozwala sprawdzić pierwszość liczb do 4.
To wystarczy, żeby stwierdzić, że 3 jest liczbą pierwszą.
W każdej chwili, największa obliczona dotąd liczba pierwsza <math>p</math> wystarcza do przetestowania pierwszości liczb do <math>p^2</math> włącznie.
Natomiast kolejna liczba pierwsza <math>p'</math> spełnia <math>p' < 2p \le p^2</math>.
Tak więc, każda kolejna wyznaczona liczba pierwsza zwiększa możliwy zakres testowania pierwszości o więcej niż odległość do kolejnej liczby pierwszej.
</p>


<center>      
<p>
<math>
<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250">
\begin{matrix}
<param name="DIR" value="images/tab4/">
\texttt{integers\_from 3}      &  & 3 & 4, 5 & 6, 7 & 8, 9, 10, 11 & 12, 13 & 14, 15, 16, 17 & \dots \\
</applet>
\texttt{filter\_stream} \dots&  & 3 & 5 & 7 & 11 & 13 & 17 & \dots \\
</p>
\texttt{primes}      & 2 & 3 & 5 & 7 & 11 & 13 & 17 & \dots \\
\end{matrix}
</math>
</center>


==Iteracje jako strumienie==
==Iteracje jako strumienie==
<p align="justify">
W przypadku definicji nie uwikłanych używaliśmy procedur iteracyjnych do zdefiniowania strumieni.
Możemy ten mechanizm odwrócić i użyć strumieni do opisania procesów iteracyjnych -- kolejne elementy strumienia mogą reprezentować kolejne kroki iteracji.
</p>


W przypadku definicji nie uwikłanych używaliśmy procedur iteracyjnych do zdefiniowania strumieni. Możemy ten mechanizm odwrócić i użyć strumieni do opisania procesów iteracyjnych - kolejne elementy strumienia mogą reprezentować kolejne kroki iteracji. Oto przykład, strumień kolejnych przybliżeń pierwiastka, metodą Newtona:  
=== Metoda Newtona przybliżania pierwiastków ===
Oto przykład, strumień kolejnych przybliżeń pierwiastka kwadratowego, metodą Newtona:  
        
        
  '''let''' sqrt_improve x g = (g +. x /. g) /. 2.0;;
  '''let''' sqrt_improve x g = (g +. x /. g) /. 2.0;;
  &nbsp;&nbsp;
  ''val sqrt_improve : float -> float -> float = <fun>''
  '''let''' sqrt_stream x =  
  '''let''' sqrt_stream x =  
&nbsp;&nbsp;'''let''' '''rec''' guesses =  
  '''let''' '''rec''' guesses =  
&nbsp;&nbsp;&nbsp;&nbsp;Cons (1.0,  
    Cons (1.0,  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;stream_map (sqrt_improve x) guesses)
      stream_map (sqrt_improve x) guesses)
  &nbsp;&nbsp;'''in''' guesses;;
  '''in''' guesses;;
     
''val sqrt_stream : float -> float stream = <fun>''
Spróbujmy przybliżyć liczbę <math>\pi</math>.
   
sample (sqrt_stream 42.0);;
''- : float list = ''
''[1.; 21.5; 11.7267441860465116; 7.65415047676148141; ''
'' 6.57068474328365859; 6.48135630633341897; 6.48074072764349385; 6.48074069840786; ''
'' 6.48074069840786; 6.48074069840786; 6.48074069840786; 6.48074069840786]''
 
=== Przybliżenia <math>\pi</math> ===
<p align="justify">
Skonstruujemy strumień przybliżeń liczby <math>\pi</math>.
Użyjemy do tego celu szeregu zbieżnego do <math>\frac{\pi}{4}</math>:
Użyjemy do tego celu szeregu zbieżnego do <math>\frac{\pi}{4}</math>:
</p>
<center><math>\frac{\pi}{4} = 1 - \frac{1}{3} + \frac{1}{5} - \frac{1}{7} + \cdots</math></center>
<p align="justify">
Najpierw definiujemy strumień mianowników <math>(1, -3, 5, -7, \dots)</math>, następnie przekształcamy go w strumień sumowanych ułamków, po czym wyznaczamy strumień sum częściowych naszego szeregu.
</p>


<center><math>\frac{\pi}{4} = 1 - \frac{1}{3} + \frac{1}{5} - \frac{1}{7} + \cdots </math></center>
     
'''let''' scale_stream c s =
&nbsp;&nbsp;stream_map (function x -> x *. c) s;;
&nbsp;&nbsp;
  '''let''' pi_summands =  
  '''let''' pi_summands =  
&nbsp;&nbsp;'''let''' succ x =  
  '''let''' succ x =  
&nbsp;&nbsp;&nbsp;&nbsp;'''if''' x > 0.0 '''then'''  
    '''if''' x > 0.0 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-.x -. 2.0
      -.x -. 2.0
&nbsp;&nbsp;&nbsp;&nbsp;'''else'''
    '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-.x +. 2.0
      -.x +. 2.0
&nbsp;&nbsp;'''in'''
  '''in'''
&nbsp;&nbsp;&nbsp;&nbsp;'''let''' '''rec''' s =  
    '''let''' '''rec''' s =  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (1.0, stream_map succ s)
      Cons (1.0, stream_map succ s)
&nbsp;&nbsp;&nbsp;&nbsp;'''in'''
    '''in'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;stream_map (fun x -> 1.0 /. x) s;;
      stream_map (fun x -> 1.0 /. x) s;;
  &nbsp;&nbsp;
  ''val pi_summands : float stream = Cons (1., <lazy>)''
sample pi_summands;;
''- : float list =''
''[1.; -0.333333333333333315; 0.2; -0.142857142857142849; 0.111111111111111105;''
'' -0.0909090909090909116; 0.0769230769230769273; -0.0666666666666666657; ''
'' 0.0588235294117647051; -0.0526315789473684181; 0.0476190476190476164;''
''  -0.0434782608695652162]''
'''let rec''' add_float_streams s1 s2 = stream_map2 (+.) s1 s2;;
''val add_float_streams : float stream -> float stream -> float stream = <fun>''
  '''let''' partial_sums s =  
  '''let''' partial_sums s =  
&nbsp;&nbsp;'''let''' '''rec''' ps =  
  '''let''' '''rec''' ps =  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (head s, add_float_streams (tail s) ps)
    Cons (head s, add_float_streams (tail s) ps)
&nbsp;&nbsp;'''in''' ps;;
  '''in''' ps;;
  &nbsp;&nbsp;&nbsp;&nbsp;
  ''val partial_sums : float stream -> float stream = <fun>''
'''let''' pi_stream = scale_stream 4.0 (partial_sums (pi_summands 1.0));;
     
Taki strumień jest co prawda zbieżny, ale bardzo wolno. Po zsumowaniu pierwszych 1000 elementów ustalone są dopiero trzy pierwsze cyfry: <math>3.14</math>. Jest to dosyć oczywiste, jeżeli zauważymy, że suma pierwszych <math>n</math> elementów jest obarczona błędem rzędu <math>\frac{1}{n}</math>
     
Można "przyspieszyć" zbieżność tego szeregu stosując  "akcelerator Eulera". Jeśli <math>S_n</math> jest sumą pierwszych <math>n</math> wyrazów szeregu, to szereg zakcelerowany ma postać.


<center><math>S_{n+1} - \frac{(S_{n+1} - S_n)^2}{S_{n-1} - 2 S_n + S_{n+1}}</math></center>  
<p align="justify">
Strumień przybliżający <math>\pi</math> uzyskujemy mnożąc elementy strumienia sum częściowych przez 4.
</p>


Działa on dobrze dla ciągów o malejących modułach błędów przybliżenia.
'''let''' pi_stream = scale_stream 4.0 (partial_sums (pi_summands));;
     
''val pi_stream : float stream = Cons (4., <lazy>)''
Dlaczego on działa? Przedstawmy kolejne sumy częściowe szeregu w postaci granica plus błąd: <math>S_n = x + e_n</math>, <math>|e_n| > |e_n+1| > 0</math>. Wówczas elementy przyspieszonego szeregu mają postać:
sample pi_stream;;
''- : float list =''
''[4.; 2.66666666666666696; 3.46666666666666679; 2.89523809523809561;''
'' 3.33968253968254025; 2.97604617604617649; 3.28373848373848443;''
'' 3.01707181707181782; 3.25236593471887669; 3.04183961892940324;''
'' 3.23231580940559393; 3.05840276592733318]''


<center><math>\begin{matrix}x + e_{n+1} - \frac{(x + e_{n+1} - x - e_n)^2}{x + e_{n-1} -2x - 2e_n + x + e_{n+1}} 
<p align="justify">
= x + e_{n+1} - \frac{(e_{n+1} - e_n)^2}{e_{n-1} - 2e_n + e_{n+1}} = \\
Taki strumień jest co prawda zbieżny, ale bardzo wolno.
= x + \frac{e_{n+1}e_{n-1} - 2 e_{n+1} e_n +e_{n+1}^2 - e_{n+1}^2 + 2 e_n e_{n+1} - e_n^2 }{e_{n-1} - 2e_n + e_{n+1}} 
Po zsumowaniu pierwszych 1000 elementów ustalone są dopiero trzy pierwsze cyfry: <math>3.14</math>.
= x + \frac{e_{n+1}e_{n-1} - e_n^2}{e_{n-1} - 2e_n + e_{n+1}} \end{matrix}</math></center>
Jest to dosyć oczywiste, jeżeli zauważymy, że suma pierwszych <math>n</math> elementów jest obarczona
błędem rzędu <math>\frac{1}{n}</math>.
</p>


Jeżeli np. <math>|e_n| = e \cdot c^n</math> i znaki <math>e_n</math> są takie same lub  naprzemienne, to <math>e_{n+1}e_{n-1} - e_n^2 = 0</math>, czyli ciąg natychmiast osiąga granicę.
<p align="justify">
Można "przyspieszyć" zbieżność tego szeregu stosując tzw. "akcelerator Eulera".  
Jeśli <math>S_n</math> jest sumą pierwszych <math>n</math> wyrazów szeregu, to szereg zakcelerowany ma postać:
</p>


.... sprawdzić, kiedy można strumień wiele razy przyspieszać ...
<center><math>S_{n+1} - \frac{(S_{n+1} - S_n)^2}{S_{n-1} - 2 S_n + S_{n+1}}</math></center>


Oto implementacja akceleratora Eulera:
<p align="justify">
Działa on dobrze np. dla ciągów naprzemiennie zbieżnych, ale nie tylko.
Oto jego implementacja:
</p>
        
        
  '''let''' euler_transform st =
  '''let''' euler_transform st =
&nbsp;&nbsp;'''let''' transform s =  
  '''let''' transform s =  
&nbsp;&nbsp;&nbsp;&nbsp;'''let''' s0 = stream_ref s 0
    '''let''' s0 = stream_ref s 0
&nbsp;&nbsp;&nbsp;&nbsp;'''and''' s1 = stream_ref s 1
    '''and''' s1 = stream_ref s 1
&nbsp;&nbsp;&nbsp;&nbsp;'''and''' s2 = stream_ref s 2
    '''and''' s2 = stream_ref s 2
&nbsp;&nbsp;&nbsp;&nbsp;'''in''' (s2 -. square (s2 -. s1) /. (s0 -. 2.0 *. s1 +. s2))
    '''in''' (s2 -. (s2 -. s1) *. (s2 -. s1) /. (s0 -. 2.0 *. s1 +. s2))
  &nbsp;&nbsp;'''in'''
  '''in'''
  &nbsp;&nbsp;&nbsp;&nbsp;stream_map transform (tails st);;
    stream_map transform (tails st);;
     
''val euler_transform : float stream -> float stream = <fun>''
Taki strumień jest już zbieżny w rozsądnym czasie. Przeliczenie 1000 elementów przyspieszonego szeregu daje przybliżenie <math>3.141592653</math>. Jeszcze lepsze wyniki daje wielokrotne przyspieszanie. Skonstruujmy strumień kolejnych przyspieszeń strumienia sum częściowych i wybierzemy z niego strumień pierwszych elementów:
   
     
sample (euler_transform pi_stream);;
''- : float list =''
''[3.16666666666666696; 3.13333333333333375; 3.14523809523809561;''
  '' 3.13968253968254; 3.14271284271284346; 3.14088134088134163;''
''3.14207181707181782; 3.14125482360776553; 3.14183961892940333;''
''3.1414067184965031; 3.14173609926066666; 3.14147968900425623]''
 
<p align="justify">
Taki strumień jest już zbieżny w rozsądnym czasie.  
Przeliczenie 1000 elementów przyspieszonego szeregu daje przybliżenie <math>3.141592653</math>.  
Jeszcze lepsze wyniki daje wielokrotne przyspieszanie.  
Skonstruujmy strumień kolejnych przyspieszeń strumienia sum częściowych i wybierzemy z niego strumień pierwszych elementów:
</p>
 
  '''let''' '''rec''' pi_table =  
  '''let''' '''rec''' pi_table =  
&nbsp;&nbsp;Cons (pi_stream, stream_map euler_transform pi_table);;
  Cons (pi_stream, stream_map euler_transform pi_table);;
  &nbsp;&nbsp;
  ''val pi_table : float stream stream = Cons (Cons (4., <lazy>), <lazy>)''
  '''let''' pi_stream_acc = stream_map head pi_table;;
  '''let''' pi_stream_acc = stream_map head pi_table;;
  &nbsp;&nbsp;
  ''val pi_stream_acc : float stream = Cons (4., <lazy>)''
  '''let''' pi = stream_ref pi_stream_acc 8;;
sample pi_stream_acc;;
''- : float list =''
''[4.; 3.16666666666666696; 3.14210526315789496; 3.14159935731900486;''
'' 3.14159271403377849; 3.14159265397529275; 3.14159265359117645;''
'' 3.14159265358977802; 3.14159265358979534; 3.14159265358979489; nan; nan]''
  '''let''' pi = stream_ref pi_stream_acc 9;;
''- : float = 3.14159265358979489''
        
        
Taki strumień już w 8 elemencie osiąga granice precyzji arytmetycznej.
<p align="justify">
Jak widać, już dziewiąty element takiego strumienia stanowi dobre przybliżenie <math>\pi</math>,
gdyż błąd jest dopiero na 15 miejscu po przecinku.
Dalsze przybliżanie nie daje już rezultatów, gdyż ze względu na ograniczoną precyzję obliczeń,
w mianownikach liczonych ułamków pojawiają sie zera.
</p>
 
=== Trójkąt Pascala ===
<p align="justify">
Strumień strumieni możemy sobie przedstawić jako nieskończoną dwuwymiarową tablicę elementów.
Można więc zdefiniować trójkąt Pascala, jako strumień strumieni liczb całkowitych.
</p>
 
<p align="justify">
Zdefiniujemy go w sposób uwikłany.
Pierwsza kolumna zawiera same jedynki.
Każda kolejna kolumna zaczyna się od jedynki, a pozostałe elementy uzyskujemy sumując poprzedzający element w tej samej kolumnie oraz element na tej samej pozycji w poprzedzającej kolumnie.
</p>
 
<p align="justify">
W pewnym sensie, wyraziliśmy tutaj dwie zagnieżdżone nieskończone pętle:
jedna obliczająca kolejne elementy strumieni, a druga obliczająca kolejne strumienie.
Dzięki odroczeniu obliczania elementów strumieni, obie pętle wykonują faktycznie tylko tyle kroków, ile jest
konieczne do obliczenia badanych elementów.
</p>
 
'''let rec''' pascal =
  '''let''' next s =
    '''let rec''' wyn =
      Cons (1, add_int_streams (tail s) wyn)
    '''in'''
      wyn
  '''in'''
    Cons (ones, stream_map next pascal);;
''val pascal : int stream stream = Cons (Cons (1, <lazy>), <lazy>)''
List.map sample (sample pascal);;
''- : int list list =
''[[1;  1;  1;  1;    1;    1;    1;    1;    1;      1;      1;      1];''
'' [1;  2;  3;  4;    5;    6;    7;    8;    9;    10;    11;    12];''
'' [1;  3;  6;  10;  15;  21;    28;    36;    45;    55;    66;    78];''
'' [1;  4; 10;  20;  35;  56;    84;  120;  165;    220;    286;    364];''
'' [1;  5; 15;  35;  70;  126;  210;  330;  495;    715;  1001;  1365];''
'' [1;  6; 21;  56;  126;  252;  462;  792;  1287;  2002;  3003;  4368];''
'' [1;  7; 28;  84;  210;  462;  924;  1716;  3003;  5005;  8008;  12376];''
'' [1;  8; 36; 120;  330;  792;  1716;  3432;  6435;  11440;  19448;  31824];''
'' [1;  9; 45; 165;  495; 1287;  3003;  6435; 12870;  24310;  43758;  75582];''
'' [1; 10; 55; 220;  715; 2002;  5005; 11440; 24310;  48620;  92378; 167960];''
'' [1; 11; 66; 286; 1001; 3003;  8008; 19448; 43758;  92378; 184756; 352716];''
'' [1; 12; 78; 364; 1365; 4368; 12376; 31824; 75582; 167960; 352716; 705432]]''
 
 
<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="300">
<param name="DIR" value="images/tab5/">
</applet>
 
=== Scalanie strumieni ===
<p align="justify">
Zdefiniujmy pożyteczną procedurę, która scala dwa strumienie posortowane niemalejąco w jeden:
</p>
 
'''let''' '''rec''' merge s1 s2 =
  '''if''' empty s1 '''then''' s2 '''else'''
  '''if''' empty s2 '''then''' s1 '''else'''
    '''let''' h1 = head s1
    '''and''' h2 = head s2
    '''in'''
      '''if''' h1 < h2 '''then'''
        Cons (h1, merge (tail s1) s2)
      '''else''' 
        Cons (h2, merge s1 (tail s2));;
''val merge : 'a stream -> 'a stream -> 'a stream = <fun>''
 
<p align="justify">
Procedurę tę możemy wykorzystać do zdefiniowania w sposób uwikłany strumienia postaci:
jedna jedynka, dwie dwójki, trzy trójki itd.  
</p>


Przykład:
'''let rec''' s = Cons (1, merge (stream_map (fun x -> x+1) s) (integers_from 2));;
Trójkąt Pascala można sobie przedstawić jako strumień strumieni liczb całkowitych. Zdefiniuj w sposób uwikłany taki strumień.
''val s : int stream = Cons (1, <lazy>)''
sample s;;
  ''- : int list = [1; 2; 2; 3; 3; 3; 4; 4; 4; 4; 5; 5]''


<center>
=== Strumienie par - liczby Ramanujana ===
<math>\begin{matrix}
<p align="justify">
1 & 1 & 1 & 1 & 1 & 1 & \dots \\
Skonstruujemy teraz strumień liczb Ramanujana.
1 & 2 & 3 & 4 & 5 & 6 & \dots \\
Liczby Ramanujana to takie liczby naturalne, które można przedstawić jako sumy sześcianów dwóch liczb naturalnych na dwa różne sposoby.
1 & 3 & 6 & 10& 15& 21& \dots \\
Pierwsze z tych liczb, to: 1729, 4104, 13832, 20683.
1 & 4 & 10& 20& 35& 56& \dots \\
</p>
1 & 5 & 15& 35& 70&126& \dots \\
1 & 6 & 21& 56&126&252& \dots \\
\vdots & \vdots & \vdots & \vdots & \vdots & \vdots &
\end{matrix}</math></center>


==Strumienie par - liczby Ramanujana==
<p align="justify">
Nasza konstrukcja będzie przebiegać następująco:
* każdej parze liczb naturalnych przypiszemy wagę równą sumie ich sześcianów,
* utworzymy strumień nieuporządkowanych par liczb naturalnych, uporządkowany wg. niemalejących wag par,
* pary o równych wagach znajdą się obok siebie, co ułatwi wychwycenie takich wag, którym odpowiada więcej niż jedna para,
* wynik będą stanowić takie powtarzające się wagi.
</p>


Przedstawimy teraz jako przykład operowania strumieniami, konstrukcję strumienia liczb Ramanujana. Liczby Ramanujana to takie liczby naturalne, które można przedstawić jako sumy sześcianów dwóch liczb naturalnych na dwa różne sposoby. Pierwsze z tych liczb, to: 1729, 4104, 13832, 20683. Niech waga pary liczb naturalnych to będzie suma sześcianów jej składowychPomysł konstrukcji strumienia liczb Ramanujana polega na skonstruowaniu strumienia (nieuporządkowanych) par liczb naturalnych uporządkowanego wg. niemalejących wag, wychwyceniu par o powtarzających się wagach i wypisaniu tych wag.
<p align="justify">
Pary nieuporządkowane będziemy reprezentować za pomocą par uporządkowanych, w
których pierwsza współrzędna nie przekracza drugiej.  
Istotny będzie fakt, że wagi par rosną wraz ze zwiększeniem jednego z elementów pary.  
Wagi par definiujemy następująco:
</p>
 
'''let''' cube x = x * x * x;;
''val cube : int -> int = <fun>''
   
'''let''' weight (x, y) = cube x + cube y;;
''val weight : int * int -> int = <fun>''


Następująca procedura łączy dwa strumienie uporządkowane wg. niemalejących wag elementów w jeden uporządkowany strumień. Waga jest określona w postaci funkcji <tt>w</tt>    
<p align="justify">
Następująca procedura scala dwa różnowartościowe strumienie, uporządkowane wg. niemalejących wag elementów
w jeden uporządkowany strumień różnowartościowy.
Elementy powtarzające się są usuwane.  
Waga jest określona w postaci funkcji <tt>w</tt>.
Bedziemy używać tej procedury do scalania par liczb naturalnych.
</p>


  '''let''' '''rec''' merge_weighted w s1 s2 =  
  '''let''' '''rec''' merge_weighted w s1 s2 =  
&nbsp;&nbsp;'''if''' empty s1 '''then''' s2 '''else'''
  '''if''' empty s1 '''then''' s2 '''else'''
&nbsp;&nbsp;'''if''' empty s2 '''then''' s1 '''else'''  
  '''if''' empty s2 '''then''' s1 '''else'''  
&nbsp;&nbsp;&nbsp;&nbsp;'''let''' h1 = head s1  
    '''let''' h1 = head s1  
&nbsp;&nbsp;&nbsp;&nbsp;'''and''' h2 = head s2
    '''and''' h2 = head s2
&nbsp;&nbsp;&nbsp;&nbsp;'''in'''  
    '''in'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' w h1 < w h2 '''then'''  
      '''if''' w h1 < w h2 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (h1, merge_weighted w (tail s1) s2)
        Cons (h1, merge_weighted w (tail s1) s2)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''' '''if''' w h1 > w h2 '''then'''  
      '''else''' '''if''' w h1 > w h2 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (h2, merge_weighted w s1 (tail s2))
        Cons (h2, merge_weighted w s1 (tail s2))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''' '''if''' h1 = h2 '''then'''  
      '''else''' '''if''' h1 = h2 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (h1, merge_weighted w (tail s1) (tail s2))
        Cons (h1, merge_weighted w (tail s1) (tail s2))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else'''  
      '''else'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (h1, Cons (h2, merge_weighted w (tail s1) (tail s2)));;
        Cons (h1, Cons (h2, merge_weighted w (tail s1) (tail s2)));;
     
''val merge_weighted : ('a -> 'b) -> 'a stream -> 'a stream -> 'a stream = <fun>''
Następujące definicje określają wagi par oraz funkcję tworzącą strumień par uporządkowanych wg. wag.
 
<p align="justify">
Niech <tt>s</tt> będzie rosnącym strumieniem liczb.
Poniższa procedura tworzy strumień (nieuporządkowanych) par liczb z <tt>s</tt>, uporządkowany wg. niemalejących wag tych par.  
Istotne jest tu założenie, że wraz ze wzrostem jednej z liczb, waga pary rośnie.
</p>
 
<p align="justify">
Pierwszym elementem strumienia wynikowego jest oczywiście para <tt>(head s, head s)</tt>.
Pozostałe pary otrzymujemy scalając strumień par zawierających <tt>head s</tt> i jakąś inną liczbę,
ze strumieniem par nie zawierających <tt>head s</tt>.
Interesujący nas strumień par, to <tt>pairs nats</tt>.
</p>
        
        
'''let''' cube x = x * x * x;;
&nbsp;&nbsp;
'''let''' weight (x, y) = cube x + cube y;;
&nbsp;&nbsp;
  '''let''' '''rec''' pairs s =  
  '''let''' '''rec''' pairs s =  
&nbsp;&nbsp;Cons ((head s, head s),  
  Cons ((head s, head s),  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;merge_weighted  
        merge_weighted  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;weight
          weight
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(stream_map (function x -> (head s, x)) (tail s))
          (stream_map (function x -> (head s, x)) (tail s))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(pairs (tail s)));;
          (pairs (tail s)));;
     
''val pairs : int stream -> (int * int) stream = <fun>''
Następująca procedura pozostawia jedynie reprezentantów sekwencji elementów o powtarzających się wagach.
     
sample (pairs nats);;
''- : (int * int) list = ''
''[(0, 0); (0, 1); (1, 1); (0, 2); (1, 2); (2, 2);
'' (0, 3); (1, 3); (2, 3); (3, 3); (0, 4); (1, 4)]''
 
<p align="justify">
Poniższa procedura wychwytuje powtarzające się wagi par.  
</p>
 
  '''let''' '''rec''' non_uniq w s =  
  '''let''' '''rec''' non_uniq w s =  
&nbsp;&nbsp;'''let''' '''rec''' skip we s =
  '''let''' '''rec''' skip we s =
&nbsp;&nbsp;&nbsp;&nbsp;'''if''' empty s '''then''' Nil  
    '''if''' empty s '''then''' Nil  
&nbsp;&nbsp;&nbsp;&nbsp;'''else''' '''if''' we = w (head s) '''then''' skip we (tail s)
    '''else''' '''if''' we = w (head s) '''then''' skip we (tail s)
&nbsp;&nbsp;&nbsp;&nbsp;'''else''' s
    '''else''' s
&nbsp;&nbsp;'''in'''
  '''in'''
&nbsp;&nbsp;&nbsp;&nbsp;'''if'' empty s '''then''' Nil  
    '''if''' empty s '''then''' Nil  
&nbsp;&nbsp;&nbsp;&nbsp;'''else''' '''if''' empty (tail s) '''then''' Nil  
    '''else''' '''if''' empty (tail s) '''then''' Nil  
&nbsp;&nbsp;&nbsp;&nbsp;'''else'''
    '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''let''' h1 = head s
      '''let''' h1 = head s
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''and''' h2 = head (tail s)
      '''and''' h2 = head (tail s)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''in'''  
      '''in'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''if''' w h1 = w h2 '''then'''  
        '''if''' w h1 = w h2 '''then'''  
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Cons (w h1, non_uniq w (skip (w h1) s))
          Cons (w h1, non_uniq w (skip (w h1) s))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'''else''
        '''else'''
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;non_uniq w (tail s);;
          non_uniq w (tail s);;
     
''val non_uniq : ('a -> 'b) -> 'a stream -> 'b stream = <fun>''
 
Strumień liczb Ramanujana możemy zdefiniować następująco:
Strumień liczb Ramanujana możemy zdefiniować następująco:
        
        
  '''let''' ramanujan =  
  '''let''' ramanujan = non_uniq weight (pairs nats);;
  &nbsp;&nbsp;'''in''' non_uniq weight (pairs nats);;
''val ramanujan : int stream = Cons (1729, <lazy>)''
sample ramanujan;;
''- : int list = [1729; 4104; 13832; 20683; 32832; 39312; 40033; 46683; 64232; 65728; 110656; 110808]''
 
=== Back-tracking + kontynuacje = strumień wyników ===
<p align="justify">
Pokażemy teraz jak łącząc technikę back-trackingu i kontynuacji możemy wygenerować wynik w postaci strumienia rozwiązań.
Rozwiązanie to jest o tyle ciekawe, że poszukiwania kolejnych rozwiązań są prowadzone w miarę, jak ze strumienia
wyników wyjmujemy kolejne elementy.
Technikę tę pokażemy na dobrze znanym przykładzie problemu ośmiu hetmanów.
</p>
 
Predykat <tt>szach</tt> określa czy hetmany stojące na dwóch określonych pozycjach szachują się nawzajem.
 
'''let''' szach (x1, y1) (x2, y2) = x1=x2 or y1=y2 or x1+y1=x2+y2 or x1-y1=x2-y2;;
''val szach : int * int -> int * int -> bool = <fun>''
 
Predykat <tt>mozna_dostawic</tt> określa, czy do już stojących na szachownicy hetmanów można dostawić kolejnego.
 
  '''exception''' Szach;;
''exception Szach''
'''let''' mozna_dostawic h het =
  '''try''' fold_left (fun ok x -> '''if''' szach h x '''then raise''' Szach '''else''' ok) true het
  '''with''' Szach -> false;;
''val mozna_dostawic : int * int -> (int * int) list -> bool = <fun>''
 
Właściwy back-tracking realizuje procedura <tt>dostaw</tt>, która przeszukuje wszystkie możliwe ustawienia
hetmanów na planszy.


==Co jeszcze ...==
'''let''' hetman n =
Strumienie jako reprezentacja danych, np. szeregów potęgowych. Operacje na funkcjach reprezentowanych w taki sposób.
  '''let rec''' dostaw het k l cont =
    '''if''' k = 0 '''then''' Cons(het, force cont) '''else'''
    '''if''' l = 0 '''then''' force cont '''else'''
    '''if''' mozna_dostawic (k, l) het '''then'''
      dostaw ((k,l)::het) (k-1) n ('''lazy''' (dostaw het k (l-1) cont))
    '''else''' dostaw het k (l-1) cont
  '''in''' dostaw [] n n ('''lazy''' Nil);;


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


[[Programowanie funkcyjne/Strumienie/Ćwiczenia        | Laboratoria i ćwiczenia do wykładu]]
[[Programowanie funkcyjne/Strumienie/Ćwiczenia        | Ćwiczenia]]

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

Wstęp

Przedstawiając konstrukcje imperatywne, powiedzieliśmy, że w programowaniu imperatywnym upływ czasu w świecie rzeczywistym jest modelowany przez upływ czasu w programie komputerowym. W "czystym" programowaniu funkcyjnym nie ma zmiennych, ani zmian stanów. Trudno też mówić o upływie czasu w programie. Wszak program składa się z implementacji pojęć matematycznych. Jedyna zależność związana z czasem polega na tym, że pojęcia wykorzystywane muszą być zdefiniowane przed pojęciami korzystającymi z nich.

Strumienie to po prostu ciągi wartości, być może nieskończone. Programowanie strumieniowe pozwala jednak na utworzenie związku między czasem i obliczeniami.

W fizyce mamy pojęcie czasoprzestrzeni. Sama czasoprzestrzeń nie zmienia się w czasie -- jest to twór matematyczny ujmujący upływ czasu i historię całej przestrzeni. Historia pojedynczej cząstki jest reprezentowana przez jej trajektorię w czasoprzestrzeni, nazywaną linią świata cząstki.

Jak zobaczymy, strumienie mogą być użyte do przedstawiania historii obliczeń iteracyjnych. Strumień może reprezentować całą, być może nieskończoną historię pewnych obliczeń. Pozwala to "jednym rzutem oka" spojrzeć na całą historię obliczenia. Jak zobaczymy, pozwoli to również na implementowanie pewnych ciekawych operacji na "liniach świata obliczeń".

Wiele z podanych w tym wykładzie przykładów pochodzi z [AS], przy czym zostały przeniesione z języka Scheme do Ocamla.

Definiowanie form specjalnych

Jak już pisaliśmy wcześniej, 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. instrukcja warunkowa if-then-else --- w zależności od wartości warunku, tylko jeden z pozostałych członów jest obliczany.

Czy można takie formy specjalne zaimplementować samemu? Tak. Potrzeba do tego dwóch rzeczy. Musimy umieć odroczyć moment obliczania wartości i wprowadzać własne makrodefinicje.

O uleniwianiu i odraczaniu obliczeń mówiliśmy w poprzednim wykładzie. W Ocamlu możemy nie tylko definiować własne makrodefinicje, ale wręcz zmieniać składnię języka (sic!). Za analizę składniową odpowiedzialny jest preprocesor P4. Nie będziemy tu mówić o nim, gdyż wykracza to poza zakres tego kursu. Jednak raz użyjemy definicji wprowadzającej prostą makrodefinicję, która realizuje formę specjalną.

Implementacja strumieni

Formalnie strumień to ciąg elementów --- być może nieskończony. Pomysł polega jednak na tym, żeby był on "leniwy", tzn. obliczane były tylko potrzebne wartości. Przyjmujemy przy tym, że wartość stają się potrzebne zgodnie z ich kolejnością występowania w strumieniu. Tak więc w każdej chwili strumień składa się ze skończonego ciągu obliczonych (potrzebnych) wartości oraz odroczonego (być może nieskończonego) ciągu pozostałych elementów. Implementując strumienie korzystamy z uleniwiania (zaimplementowanego przez moduł Lazy). Strumień, jeśli nie jest pusty, to para: wartość pierwszego elementu i odroczony strumień pozostałych wartości.

type 'a stream = Nil | Cons of 'a * 'a stream Lazy.t;;
type 'a stream = Nil | Cons of 'a * 'a stream Lazy.t

let empty s = s = Nil;;
val empty : 'a stream -> bool = <fun>

Podstawowe "cegiełki" do budowy strumieni są dokładnie takie same jak do budowy list: pusty strumień oraz dołączenie elementu na początku strumienia. Dwie podstawowe operacje służące do dekonstrukcji strumieni, to procedury zwracające głowę i ogon danego strumienia.

let head s = 
  match s with
    Nil -> failwith "Empty" |
    Cons (h, _) -> h;;
val head : 'a stream -> 'a = <fun>

let tail s = 
  match s with
    Nil -> failwith "Empty" |
    Cons (_, t) -> Lazy.force t;;
val tail : 'a stream -> 'a stream = <fun>

Tak zdefiniowany typ danych jest trochę niewygodny w użyciu, gdyż zawsze gdy używamy Cons, powinniśmy go używać razem z lazy. Aby ułatwić sobie życie, zdefiniujemy Cons jako makro, które przed drugim swoim parametrem dodaje słowo kluczowe lazy.

Cons(x,y)Cons(x,lazy y)

Oto niezbędne "zaklęcie", które podajemy bez wyjaśnień. Zainteresowanych odsyłamy do dokumentacji preprocesora P4.

#load "camlp4o.cma";;
#load "pa_extend.cmo";;
#load "q_MLast.cmo";;

EXTEND
  Pcaml.expr: LEVEL "simple"
  [ [ UIDENT "Cons"; param=SELF ->
        let h,l =
          match param with <:expr< ($list:[h;l]$) >> -> (h,l)
                           | _ -> raise Not_found
        in
          <:expr< Cons ($h$) (lazy $l$)>> ] ]
  ;
END;;

Operacje na strumieniach

Mając do dyspozycji takie konstruktory i selektory możemy zdefiniować kilka pomocniczych operacji na strumieniach:

let rec const_stream c = 
  Cons (c, const_stream c);; 
val const_stream : 'a -> 'a stream = <fun>

Definiuje nieskończony strumień, którego wszystkie elementy są równe c.


let ones = const_stream 1;;
val ones : int stream = Cons (1, <lazy>)

To jest nieskończony strumień jedynek.


let rec filter_stream p s = 
  if empty s then 
    Nil
  else 
    let h = head s 
    and t = tail s
    in 
      if p h then 
        Cons (h, filter_stream p t)
      else
        filter_stream p t;;
val filter_stream : ('a -> bool) -> 'a stream -> 'a stream = <fun>

Działa podobnie jak procedura filter dla list, tworzy strumień złożony wyłącznie z tych elementów danego strumienia, które spełniają podany predykat. Jeśli strumień będący parametrem jest nieskończony, to nieskończenie wiele jego elementów powinno spełniać predykat, tak żeby strumień wynikowy był również nieskończony.


let rec stream_ref s n = 
  if n = 0 then 
    head s
  else
    stream_ref (tail s) (n - 1);;
val stream_ref : 'a stream -> int -> 'a = <fun>

Zwraca element strumienia z podanej pozycji. Pozycje w strumieniu są indeksowane od 0.


let rec stream_map f s = 
  if empty s then 
    Nil
  else
    Cons (f (head s), stream_map f (tail s));;
val stream_map : ('a -> 'b) -> 'a stream -> 'b stream = <fun>

Działa podobnie jak procedura map dla list, stosuje dane przekształcenie f do wszystkich elementów danego strumienia i tworzy strumień wyników.

let scale_stream c s = 
  stream_map (function x -> x *. c) s;;
val scale_stream : float -> float stream -> float stream = <fun>

Procedura ta mnoży elementy danego strumienia przez zadaną stałą.

let rec stream_map2 f s1 s2 = 
  if empty s1 or empty s2 then Nil 
  else Cons (f (head s1) (head s2), stream_map2 f (tail s1) (tail s2));;
val stream_map2 : ('a -> 'b -> 'c) -> 'a stream -> 'b stream -> 'c stream = <fun>

Jest to odpowiednik procedury map2 z modułu List. Pozwala ona na proste zdefiniowanie wielu operacji łączących strumienie. Na przykład, dodawanie strumieni:

let rec add_int_streams s1 s2 = stream_map2 (+) s1 s2;;
val add_int_streams : int stream -> int stream -> int stream = <fun>
 
let sample s = 
 let rec pom i s = 
   if i = 0 || empty s then []
   else head s :: pom (i-1) (tail s)
 in 
   pom 12 s;;
val sample : 'a stream -> 'a list = <fun>

Procedury tej będziemy używać do testowania konstruowanych przez nas strumieni.

sample ones;;
- : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1]
Uwaga
W tym wykladzie pokazujemy, jak zaimplementować strumienie, choć Ocaml zawiera moduł (Streams) implementujący strumienie. Jednak są to dwie różne struktury danych. Strumienie zaimplementowane w module Streams odpowiadają bardziej pojęciu strumienia w C++.

Przykłady strumieni nieskończonych

Zdefiniujmy strumień liczb naturalnych:

let rec integers_from x = 
  Cons(x, integers_from (x+1));;
val integers_from : int -> int stream = <fun>

let nats = integers_from 0;;
val nats : int stream = Cons (0, <lazy>)

sample nats;;
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11]

Podobnie możemy zdefiniować strumień liczb Fibonacciego:

let fibs = 
  let rec fibo a b = 
    Cons (a, fibo b (a+b))
  in fibo 0 1;;
val fibs : int stream = Cons (0, <lazy>)

sample fibs;;
- : int list = [0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89]

Jak widać, jeżeli jesteśmy w stanie skonstruować iteracyjną procedurę wyliczającą kolejne elementy strumienia, to tym samym jesteśmy w stanie zdefiniować strumień tych wartości. Procedura taka stanowi ukrytą w strumieniu "maszynerię", która na żądanie podaje kolejne elementy strumienia. Takie definicje strumieni nazywamy nie uwikłanymi (w odróżnieniu od uwikłanych, które poznamy dalej). Charakterystyczne dla definicji nie uwikłanych jest to, że bytem definiowanym rekurencyjnie jest procedura, a nie strumień. Strumień jest zdefiniowany dopiero w oparciu o rekurencyjnie zdefiniowaną procedurę.

Strumienie to tylko jeden ze sposobów konstruowania złożonych struktur danych. Można więc go łączyć z dowolnymi innymi sposobami konstruowania danych i tworzyć strumienie list, listy strumieni, czy nawet strumienie strumieni. Oto przykład, strumień strumieni powstałych z danego (nieskończonego) strumienia przez usunięcie coraz większej liczby początkowych elementów (poczynając od zera elementów):

let rec tails s = 
  if empty s then Cons (s, Nil) else Cons (s, tails (tail s));;
val tails : 'a stream -> 'a stream stream = <fun>

Strumień liczb pierwszych -- sito Eratostenesa

Spróbujmy skonstruować nieskończony strumień liczb pierwszych, generowanych metodą sita Eratostenesa. Sposób konstrukcji takiego strumienia możemy przedstawić w postaci schematu przypominającego schematy blokowe układów przetwarzających sygnały. Strumienie są na nim zaznaczone liniami ciągłymi, a pojedyncze wartości przerywanymi.

Poniższy diagram przedstawia budowę procedury sito implementującej pojedyncze sito. Zauważmy, że jest on rekurencyjny, tzn. procedura ta pojawia się również na tym schemacie. Odpowiada to temu, że liczby przesiane przez jedno sito trafiają na kolejene sito, itd.

Schemat ten przekłada się na następujący program:

let sitko p s = 
  filter_stream (function x -> (x mod p) <> 0) s;;
val sitko : int -> int stream -> int stream = <fun>

let rec sito s  = 
  Cons (head s, sito (sitko (head s) (tail s)));;
val sito : int stream -> int stream = <fun>
 
let primes = 
  sito (integers_from 2);;
val primes : int stream = Cons (2, <lazy>)

sample primes;;
- : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37]

Dzięki leniwości strumieni, w zależności od tego ile liczb odczytamy ze strumienia, powstanie odpowiednia liczba sit i odpowiednia ilość liczb zostanie przez nie przesiana.

Definicje uwikłane

Definiując nieskończone strumienie nie musimy tego zawsze robić poprzez podanie odpowiedniej procedury rekurencyjnej (por. integers-from, fibo, sieve). Możemy rekurencyjnie użyć strumienia do zdefiniowania jego samego.

let rec ones =
  Cons (1, ones);;

sample ones;;
- : int list = [1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1]

Do budowy bardziej skomplikowanych strumieni potrzebujemy dodatkowych operacji tworzących strumienie. Na przykład, do utworzenia strumienia liczb naturalnych czy liczb Fibonacciego potrzebujemy dodawania strumieni.

let rec nats =
  Cons (0, add_int_streams ones nats);;
val nats : int stream = Cons (0, <lazy>)

sample nats;;
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11]

Sposób obliczania kolejnych elementów strumieni zdefiniowanych w sposób uwikłany najlepiej przedstawić sobie w postaci tabelki:

<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250"> <param name="DIR" value="images/tab1/"> </applet>

Zamiast dodawania strumienia jedynek, możemy użyć operacji zwiększania o jeden:

let succ x = x + 1;;
val succ : int -> int = <fun>

let rec nats = 
  Cons(0, stream_map succ nats);;
val nats : int stream = Cons (0, <lazy>)

sample nats;;
- : int list = [0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11]

<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250"> <param name="DIR" value="images/tab2/"> </applet>

A oto uwikłana definicja liczb Fibonacciego:

let rec fibs = 
  Cons (0, Cons (1, add_int_streams fibs (tail fibs)));;
val fibs : int stream = Cons (0, <lazy>)

sample fibs;;
- : int list = [0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89]

<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250"> <param name="DIR" value="images/tab3/"> </applet>

Możemy też w uwikłany sposób zdefiniować strumień liczb pierwszych. Użyjemy do tego predykatu prime sprawdzającego, czy liczba jest pierwsza ... a predykat prime zdefiniujemy używając strumienia liczb pierwszych.

let divisible x y = x mod y = 0;;
val divisible : int -> int -> bool = <fun>

let square x = x * x;;
val square : int -> int = <fun>

let rec primes = 
  Cons (2, filter_stream prime (integers_from 3))
and prime n = 
  let rec iter ps = 
    if head ps * head ps > n then true
    else if divisible n (head ps) then false
    else iter (tail ps)
  in 
    iter primes;;
val primes : int stream = Cons (2, <lazy>)
val prime : int -> bool = <fun>

sample primes;;
- : int list = [2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37]

Całość działa poprawnie, ponieważ strumień jest leniwą strukturą danych. Pierwszy element strumienia liczb pierwszych, 2, jest dany explicite. Natomiast sprawdzając czy kolejne liczby są pierwsze, zawsze mamy już obliczone potrzebne do tego liczby pierwsze.

Konkretnie, podana explicite pierwsza liczba pierwsza, 2, pozwala sprawdzić pierwszość liczb do 4. To wystarczy, żeby stwierdzić, że 3 jest liczbą pierwszą. W każdej chwili, największa obliczona dotąd liczba pierwsza p wystarcza do przetestowania pierwszości liczb do p2 włącznie. Natomiast kolejna liczba pierwsza p spełnia p<2pp2. Tak więc, każda kolejna wyznaczona liczba pierwsza zwiększa możliwy zakres testowania pierwszości o więcej niż odległość do kolejnej liczby pierwszej.

<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="250"> <param name="DIR" value="images/tab4/"> </applet>

Iteracje jako strumienie

W przypadku definicji nie uwikłanych używaliśmy procedur iteracyjnych do zdefiniowania strumieni. Możemy ten mechanizm odwrócić i użyć strumieni do opisania procesów iteracyjnych -- kolejne elementy strumienia mogą reprezentować kolejne kroki iteracji.

Metoda Newtona przybliżania pierwiastków

Oto przykład, strumień kolejnych przybliżeń pierwiastka kwadratowego, metodą Newtona:

let sqrt_improve x g = (g +. x /. g) /. 2.0;;
val sqrt_improve : float -> float -> float = <fun>

let sqrt_stream x = 
  let rec guesses = 
    Cons (1.0, 
      stream_map (sqrt_improve x) guesses)
  in guesses;;
val sqrt_stream : float -> float stream = <fun>

sample (sqrt_stream 42.0);;
- : float list = 
[1.; 21.5; 11.7267441860465116; 7.65415047676148141; 
 6.57068474328365859; 6.48135630633341897; 6.48074072764349385; 6.48074069840786; 
 6.48074069840786; 6.48074069840786; 6.48074069840786; 6.48074069840786]

Przybliżenia π

Skonstruujemy strumień przybliżeń liczby π. Użyjemy do tego celu szeregu zbieżnego do π4:

π4=113+1517+

Najpierw definiujemy strumień mianowników (1,3,5,7,), następnie przekształcamy go w strumień sumowanych ułamków, po czym wyznaczamy strumień sum częściowych naszego szeregu.

let pi_summands = 
  let succ x = 
    if x > 0.0 then 
      -.x -. 2.0
    else
      -.x +. 2.0
  in
    let rec s = 
      Cons (1.0, stream_map succ s)
    in
      stream_map (fun x -> 1.0 /. x) s;;
val pi_summands : float stream = Cons (1., <lazy>)

sample pi_summands;;
- : float list =
[1.; -0.333333333333333315; 0.2; -0.142857142857142849; 0.111111111111111105;
 -0.0909090909090909116; 0.0769230769230769273; -0.0666666666666666657; 
 0.0588235294117647051; -0.0526315789473684181; 0.0476190476190476164;
  -0.0434782608695652162]

let rec add_float_streams s1 s2 = stream_map2 (+.) s1 s2;;
val add_float_streams : float stream -> float stream -> float stream = <fun>

let partial_sums s = 
  let rec ps = 
    Cons (head s, add_float_streams (tail s) ps)
  in ps;;
val partial_sums : float stream -> float stream = <fun>

Strumień przybliżający π uzyskujemy mnożąc elementy strumienia sum częściowych przez 4.

let pi_stream = scale_stream 4.0 (partial_sums (pi_summands));;
val pi_stream : float stream = Cons (4., <lazy>)

sample pi_stream;;
- : float list =
[4.; 2.66666666666666696; 3.46666666666666679; 2.89523809523809561;
 3.33968253968254025; 2.97604617604617649; 3.28373848373848443;
 3.01707181707181782; 3.25236593471887669; 3.04183961892940324;
 3.23231580940559393; 3.05840276592733318]

Taki strumień jest co prawda zbieżny, ale bardzo wolno. Po zsumowaniu pierwszych 1000 elementów ustalone są dopiero trzy pierwsze cyfry: 3.14. Jest to dosyć oczywiste, jeżeli zauważymy, że suma pierwszych n elementów jest obarczona błędem rzędu 1n.

Można "przyspieszyć" zbieżność tego szeregu stosując tzw. "akcelerator Eulera". Jeśli Sn jest sumą pierwszych n wyrazów szeregu, to szereg zakcelerowany ma postać:

Sn+1(Sn+1Sn)2Sn12Sn+Sn+1

Działa on dobrze np. dla ciągów naprzemiennie zbieżnych, ale nie tylko. Oto jego implementacja:

let euler_transform st =
  let transform s = 
    let s0 = stream_ref s 0
    and s1 = stream_ref s 1
    and s2 = stream_ref s 2
    in (s2 -. (s2 -. s1) *. (s2 -. s1) /. (s0 -. 2.0 *. s1 +. s2))
  in
    stream_map transform (tails st);;
val euler_transform : float stream -> float stream = <fun>

sample (euler_transform pi_stream);;
- : float list =
[3.16666666666666696; 3.13333333333333375; 3.14523809523809561;
 3.13968253968254; 3.14271284271284346; 3.14088134088134163;
3.14207181707181782; 3.14125482360776553; 3.14183961892940333;
3.1414067184965031; 3.14173609926066666; 3.14147968900425623]

Taki strumień jest już zbieżny w rozsądnym czasie. Przeliczenie 1000 elementów przyspieszonego szeregu daje przybliżenie 3.141592653. Jeszcze lepsze wyniki daje wielokrotne przyspieszanie. Skonstruujmy strumień kolejnych przyspieszeń strumienia sum częściowych i wybierzemy z niego strumień pierwszych elementów:

let rec pi_table = 
  Cons (pi_stream, stream_map euler_transform pi_table);;
val pi_table : float stream stream = Cons (Cons (4., <lazy>), <lazy>)

let pi_stream_acc = stream_map head pi_table;;
val pi_stream_acc : float stream = Cons (4., <lazy>)

sample pi_stream_acc;;
- : float list =
[4.; 3.16666666666666696; 3.14210526315789496; 3.14159935731900486;
 3.14159271403377849; 3.14159265397529275; 3.14159265359117645;
 3.14159265358977802; 3.14159265358979534; 3.14159265358979489; nan; nan]

let pi = stream_ref pi_stream_acc 9;;
- : float = 3.14159265358979489
      

Jak widać, już dziewiąty element takiego strumienia stanowi dobre przybliżenie π, gdyż błąd jest dopiero na 15 miejscu po przecinku. Dalsze przybliżanie nie daje już rezultatów, gdyż ze względu na ograniczoną precyzję obliczeń, w mianownikach liczonych ułamków pojawiają sie zera.

Trójkąt Pascala

Strumień strumieni możemy sobie przedstawić jako nieskończoną dwuwymiarową tablicę elementów. Można więc zdefiniować trójkąt Pascala, jako strumień strumieni liczb całkowitych.

Zdefiniujemy go w sposób uwikłany. Pierwsza kolumna zawiera same jedynki. Każda kolejna kolumna zaczyna się od jedynki, a pozostałe elementy uzyskujemy sumując poprzedzający element w tej samej kolumnie oraz element na tej samej pozycji w poprzedzającej kolumnie.

W pewnym sensie, wyraziliśmy tutaj dwie zagnieżdżone nieskończone pętle: jedna obliczająca kolejne elementy strumieni, a druga obliczająca kolejne strumienie. Dzięki odroczeniu obliczania elementów strumieni, obie pętle wykonują faktycznie tylko tyle kroków, ile jest konieczne do obliczenia badanych elementów.

let rec pascal = 
  let next s = 
    let rec wyn = 
      Cons (1, add_int_streams (tail s) wyn)
    in 
      wyn
  in 
    Cons (ones, stream_map next pascal);;
val pascal : int stream stream = Cons (Cons (1, <lazy>), <lazy>)

List.map sample (sample pascal);;
- : int list list =
[[1;  1;  1;   1;    1;    1;     1;     1;     1;      1;      1;      1];
 [1;  2;  3;   4;    5;    6;     7;     8;     9;     10;     11;     12];
 [1;  3;  6;  10;   15;   21;    28;    36;    45;     55;     66;     78];
 [1;  4; 10;  20;   35;   56;    84;   120;   165;    220;    286;    364];
 [1;  5; 15;  35;   70;  126;   210;   330;   495;    715;   1001;   1365];
 [1;  6; 21;  56;  126;  252;   462;   792;  1287;   2002;   3003;   4368];
 [1;  7; 28;  84;  210;  462;   924;  1716;  3003;   5005;   8008;  12376];
 [1;  8; 36; 120;  330;  792;  1716;  3432;  6435;  11440;  19448;  31824];
 [1;  9; 45; 165;  495; 1287;  3003;  6435; 12870;  24310;  43758;  75582];
 [1; 10; 55; 220;  715; 2002;  5005; 11440; 24310;  48620;  92378; 167960];
 [1; 11; 66; 286; 1001; 3003;  8008; 19448; 43758;  92378; 184756; 352716];
 [1; 12; 78; 364; 1365; 4368; 12376; 31824; 75582; 167960; 352716; 705432]]


<applet code="PSViewer" archive="images/d/dd/Psviewer.jar" align="center" width="600" height="300"> <param name="DIR" value="images/tab5/"> </applet>

Scalanie strumieni

Zdefiniujmy pożyteczną procedurę, która scala dwa strumienie posortowane niemalejąco w jeden:

let rec merge s1 s2 = 
  if empty s1 then s2 else
  if empty s2 then s1 else 
    let h1 = head s1 
    and h2 = head s2
    in 
      if h1 < h2 then 
        Cons (h1, merge (tail s1) s2)
      else  
        Cons (h2, merge s1 (tail s2));;
val merge : 'a stream -> 'a stream -> 'a stream = <fun>

Procedurę tę możemy wykorzystać do zdefiniowania w sposób uwikłany strumienia postaci: jedna jedynka, dwie dwójki, trzy trójki itd.

let rec s = Cons (1, merge (stream_map (fun x -> x+1) s) (integers_from 2));;
val s : int stream = Cons (1, <lazy>)

sample s;;
- : int list = [1; 2; 2; 3; 3; 3; 4; 4; 4; 4; 5; 5]

Strumienie par - liczby Ramanujana

Skonstruujemy teraz strumień liczb Ramanujana. Liczby Ramanujana to takie liczby naturalne, które można przedstawić jako sumy sześcianów dwóch liczb naturalnych na dwa różne sposoby. Pierwsze z tych liczb, to: 1729, 4104, 13832, 20683.

Nasza konstrukcja będzie przebiegać następująco:

  • każdej parze liczb naturalnych przypiszemy wagę równą sumie ich sześcianów,
  • utworzymy strumień nieuporządkowanych par liczb naturalnych, uporządkowany wg. niemalejących wag par,
  • pary o równych wagach znajdą się obok siebie, co ułatwi wychwycenie takich wag, którym odpowiada więcej niż jedna para,
  • wynik będą stanowić takie powtarzające się wagi.

Pary nieuporządkowane będziemy reprezentować za pomocą par uporządkowanych, w których pierwsza współrzędna nie przekracza drugiej. Istotny będzie fakt, że wagi par rosną wraz ze zwiększeniem jednego z elementów pary. Wagi par definiujemy następująco:

let cube x = x * x * x;;
val cube : int -> int = <fun>

let weight (x, y) = cube x + cube y;;
val weight : int * int -> int = <fun>

Następująca procedura scala dwa różnowartościowe strumienie, uporządkowane wg. niemalejących wag elementów w jeden uporządkowany strumień różnowartościowy. Elementy powtarzające się są usuwane. Waga jest określona w postaci funkcji w. Bedziemy używać tej procedury do scalania par liczb naturalnych.

let rec merge_weighted w s1 s2 = 
  if empty s1 then s2 else
  if empty s2 then s1 else 
    let h1 = head s1 
    and h2 = head s2
    in 
      if w h1 < w h2 then 
        Cons (h1, merge_weighted w (tail s1) s2)
      else if w h1 > w h2 then 
        Cons (h2, merge_weighted w s1 (tail s2))
      else if h1 = h2 then 
        Cons (h1, merge_weighted w (tail s1) (tail s2))
      else 
        Cons (h1, Cons (h2, merge_weighted w (tail s1) (tail s2)));;
val merge_weighted : ('a -> 'b) -> 'a stream -> 'a stream -> 'a stream = <fun>

Niech s będzie rosnącym strumieniem liczb. Poniższa procedura tworzy strumień (nieuporządkowanych) par liczb z s, uporządkowany wg. niemalejących wag tych par. Istotne jest tu założenie, że wraz ze wzrostem jednej z liczb, waga pary rośnie.

Pierwszym elementem strumienia wynikowego jest oczywiście para (head s, head s). Pozostałe pary otrzymujemy scalając strumień par zawierających head s i jakąś inną liczbę, ze strumieniem par nie zawierających head s. Interesujący nas strumień par, to pairs nats.

let rec pairs s = 
  Cons ((head s, head s), 
        merge_weighted 
          weight
          (stream_map (function x -> (head s, x)) (tail s))
          (pairs (tail s)));;
val pairs : int stream -> (int * int) stream = <fun>

sample (pairs nats);;
- : (int * int) list = 
[(0, 0); (0, 1); (1, 1); (0, 2); (1, 2); (2, 2);  
 (0, 3); (1, 3); (2, 3); (3, 3); (0, 4); (1, 4)]

Poniższa procedura wychwytuje powtarzające się wagi par.

let rec non_uniq w s = 
  let rec skip we s =
    if empty s then Nil 
    else if we = w (head s) then skip we (tail s)
    else s
  in
    if empty s then Nil 
    else if empty (tail s) then Nil 
    else
      let h1 = head s
      and h2 = head (tail s)
      in 
        if w h1 = w h2 then 
          Cons (w h1, non_uniq w (skip (w h1) s))
        else
          non_uniq w (tail s);;
val non_uniq : ('a -> 'b) -> 'a stream -> 'b stream = <fun>

Strumień liczb Ramanujana możemy zdefiniować następująco:

let ramanujan = non_uniq weight (pairs nats);;
val ramanujan : int stream = Cons (1729, <lazy>)

sample ramanujan;;
- : int list = [1729; 4104; 13832; 20683; 32832; 39312; 40033; 46683; 64232; 65728; 110656; 110808]

Back-tracking + kontynuacje = strumień wyników

Pokażemy teraz jak łącząc technikę back-trackingu i kontynuacji możemy wygenerować wynik w postaci strumienia rozwiązań. Rozwiązanie to jest o tyle ciekawe, że poszukiwania kolejnych rozwiązań są prowadzone w miarę, jak ze strumienia wyników wyjmujemy kolejne elementy. Technikę tę pokażemy na dobrze znanym przykładzie problemu ośmiu hetmanów.

Predykat szach określa czy hetmany stojące na dwóch określonych pozycjach szachują się nawzajem.

let szach (x1, y1) (x2, y2) = x1=x2 or y1=y2 or x1+y1=x2+y2 or x1-y1=x2-y2;;
val szach : int * int -> int * int -> bool = <fun>

Predykat mozna_dostawic określa, czy do już stojących na szachownicy hetmanów można dostawić kolejnego.

exception Szach;;
exception Szach

let mozna_dostawic h het =
  try fold_left (fun ok x -> if szach h x then raise Szach else ok) true het
  with Szach -> false;;
val mozna_dostawic : int * int -> (int * int) list -> bool = <fun>

Właściwy back-tracking realizuje procedura dostaw, która przeszukuje wszystkie możliwe ustawienia hetmanów na planszy.

let hetman n =
  let rec dostaw het k l cont =
    if k = 0 then Cons(het, force cont) else
    if l = 0 then force cont else
    if mozna_dostawic (k, l) het then 
      dostaw ((k,l)::het) (k-1) n (lazy (dostaw het k (l-1) cont))
    else dostaw het k (l-1) cont 
  in dostaw [] n n (lazy Nil);;

Ćwiczenia

Ćwiczenia