FunAlg - manual

Podstawy

Ponizej jest przyklad uzycia podstawowych elementow jezyka:

(* czy juz skonczono *)
let is_work_done : bool = true;;
(* podanie typu  ^^^^^^ opcjonalne *)

// ile jest piw w lodowce
let beers_to_drink = 17;;

// ile bedzie wypite
let beers_drinked_after_hour =
  if is_work_done (* czyli czy fajrant *) then
    beers_to_drink
  else
    0;;

# is_work_done : bool = true
# beers_to_drink : int = 17
# beers_drinked_after_hour : int = 17

Skladniki pierwotne

W jezyku sa dostepne predefiniowane typy, wartosci, funkcje i operatory.

Typy:

Funkcje wbudowane
Nazwa
Typ
Opis
Numeryczne
randunit => ratlosowa liczba wymierna z przedzialu <0, 1)
floorrat => intpodloga z liczby wymiernej
rat_of_intint => ratkonwersja liczby calkowitej w wymierna
Wejscie
read_boolunit => boolwartosc logiczna ("y"/"n")
read_intunit => intliczba calkowita
read_ratunit => ratliczba wymierna
read_charunit => charznak
read_strunit => strnapis
Wyjscie
print_newlineunit => unitnowy wiersz
print_boolbool => unitwartosci logiczna
print_intint => unitliczba calkowita
print_ratrat => unitliczba wymierna
print_charchar => unitznak
print_strstr => unitnapis

Infixowe operatory wbudowane
Symbol
Typ(y)
Opis
+, -, *, /, % int * int => int
rat * int => rat
int * rat => rat
rat * rat => rat
plus, minus, razy, podzielic, modulo
^ int * int => rat
rat * int => rat
int * rat => rat
rat * rat => rat
str * str => str
potegowanie / konkatenacja napisow
||, &&, $$ bool * bool => bool or, and, xor
=, <, >, <=, >=, <> a * a => bool porownanie dwoch wartosci dowolnego nie funkcyjnego typu
; unit * a => a wykonywanie krokowe

Deklaracje lokalne

Poprawnym wyrazeniem jest nastepujaca konstrukcja:

let _denote_ = _expr1_ in
  _expr2_
Na przyklad:

let trojka_wartosci = (4, 3, (true, ()));;

let pewne_wartosci =
  let (a, _, (_, b)) = trojka_wartosci in
    (a,b);;

# trojka_wartosci : int * int * (bool * unit) = (4, 3, (true, ()))
# pewne_wartosci : int * unit = (4, ())
W ogolnym wypadku dziala to tak, ze wszystkie wystapienia poszczegolnych identyfikatorow z _denote_ w _expr2_, zostaja zastapione wyliczona wczesniej wartoscia _expr1_.

Semantycznie definicja let a = b in c jest rozwijana do (&lambda a . c) b, gdzie typ argumentu a jest taki jak typ wyrazenia b.

Funkcje

Funkcje sa rownie dobrymi wartosciami jak wszystkie inne. W szczegolnosci mozna przekazywac je jako argument i zwracac jako wynik.

Funkcje definiuje sie w nastepujacy sposob:


fun _denote_ : _type_ -> _expr_
Jest to funkcja, ktora bierze argument(y) _denote_, bedace typu _type_ i zwraca wartosc _expr_.

Przykladowa definicja funkcji, a nastepnie jej aplikacja:


let my_not =
fun b : bool ->
  if b then
    false
  else
    true;;

let my_add =
fun (a, b) : int * int ->
  a + b;;

let my_true = my_not false;;

let my_two = my_add (1, 1);;

# my_not : bool => bool = FUN
# my_add : int * int => int = FUN
# my_true : bool = true
# my_two : int = 2

Bardziej zaawansowany przyklad, gdzie odpowiednia funkcja jest zwracana jako wynik:


let my_or =
fun b1 : bool ->
  if b1 then
    fun _ : bool -> true
  else
    fun b2 : bool -> b2;;

# my_or : bool => bool => bool = FUN

Typy proste

Poprawne sa nastepujace konstrukcje nowych typow: Oczywiscie powyzsze konstrukcje mozna skladac, wiec poprawnym typem prostym jest:

int * string => bool * bool * (unit => string)
Typ ten, to funkcje z par, w trojki wartosci, z ktorych ostatnia to funkcja.

Strzalka => sklada sie w prawa strone, wiec a => b => c, to typ funkcji, ktore biora argument typu a i zwracaja funkcje b => c.

Typy mozna nazywac - definiowac:


type int_pairs = int * int;;

type bools_times_int_pairs = bool * int_pairs;;

Funkcje 2

Mozemy teraz przejsc do ciekawszych przykladow funkcji:

let compose =
fun f : int => int ->
fun g : int => int ->
fun x : int ->
  f (g x);;

let imul =
fun a : int ->
fun b : int ->
  a * b;;

let inc =
fun a : int ->
  a + 1;;

let double = imul 2;;

let add_one_mul_by_two = compose double inc;;

# compose : (int => int) => (int => int) => int => int = FUN
# double : int => int = FUN
# add_one_mul_by_two : int => int = FUN
add_one_mul_by_two to funkcja, ktora liczbie n przypisze 2*(n+1).

Rekurencja

Aby zdefiniowac funkcje rekurencyjna, korzystamy z nastepujacego schematu definicji:

let rec _nazwa_ : _typ_ = _cialo_;;
Definiuje to funkcje rekurencyjna _nazwa_. Nalezy zwrocic uwage, ze w przypadku definicji rekurencyjnej konieczne jest podanie typu definiowanego wyrazenia.

Przyklad:


let rec silnia : int => int =
fun n : int ->
  if n = 0 then
    1
  else
    n * (silnia (n-1));;
    
let duzo = silnia 6;;

# silnia : int => int = FUN
# duzo : int = 720
Rekurencja jest realizowana za pomoca uleniwionego operatora punktu stalego
Y = &lambda f . (&lambda x . f (x x)) (&lambda x . f (x x)).

Wtedy definicja: let rec f : t = e;; zamieniana jest w let f = Y (&lambda f . e);;.
Po aplikacji mamy f = (&lambda f . e) (Y (&lambda f . e)) ≈ (&lambda f . e) f, czyli to o co nam chodzilo.

Typy algebraiczne

Rozwazmy nastepujaca definicje typu:

type int_list =
  | Nil : unit
  | Cons : int * int_list;;
Zdefiniowane zostaly trzy pojecia: Funkcje Nil i Cons nazywamy konstruktorami, jednak maja one wszelkie prawa zwyklych funkcji.

Nil () to lista pusta, a Cons (head, tail) to lista o glowie head i ogonie tail.

Kluczowa wlasnoscia typow algebraicznych jest mozliwosc jednoznacznego ,,destruowania'' ich wartosci:


match _expr_ with
| _cons1_ _denote1_ -> _expr1_
  ...
| _consn_ _denoten_ -> _exprn_
Wyrazenie _expr_ musi byc typu algebraicznego, ktorego konstruktory, to _cons1_..._consn_ (nie koniecznie w tej kolejnosci). Wtedy, sprawdzamy na skutek dzialania ktorego z konstruktorow powstala wartosc _expr_. Powiedzmy, ze ten konstruktor to _consi_, wtedy argumenty tego konstruktora sa denotowane z uzyciem _denotei_ i wyrazenie _expri_ jest wyliczane.

Przyklad - liczenie dlugosci listy:


let rec length : int_list => int =
fun l : int_list ->
  match l with
  | Nil _ -> 0
  | Cons (_, t) -> inc (length t);;

# length : int_list => int = FUN

Polimorfizm

Mozna definiowac explicite wartosci polimorficzne - ogolnego typu.

Przyklad:


type list [a] =
  | Nil : unit
  | Cons : a * list [a];;

type pair [a, b] = a * b;;
Powyzej zdefiniowany jest ogolny typ list. Nastepnie lista liczb calkowitych to list [int]. Takie ogolne i abstrakcyjne podejscie do rzeczy pozwala okreslic roznorodne funkcje operujace na danym typie, niezaleznie od typow skladowych:

let is_empty [a] =
fun l : list [a] ->
  match l with
  | Nil _ -> true
  | Cons _ -> false;;

let bool_list = Nil [bool] ();;

let prawda = is_empty [bool] bool_list;;

# is_empty [a] : list [a] => bool = FUN
# bool_list : list [bool] = Nil ()
# prawda : bool = true
Wazne jest, zeby odwolujac sie do typu lub wartosci polimorficznej zawsze podac explicite typy skladnikowe. W tym wypadku chodzi o fragmenty:

Szczegolnie wazne sa polimorficzne funkcje rekurencyjne:


let rec length [a] : list [a] => int =
fun l : list [a] ->
  match l with
  | Nil _ -> 0
  | Cons (h, t) -> inc (length t);;

let jeden = length [bool] (Cons [bool] (true, Nil [bool] ()));;

# length [a] : list [a] => int = FUN
# zero : int = 1
Nalezy tu zwrocic uwage, ze chociaz funkcja length jest polimorficzna, to jej rekurencyjne wywolanie ma z definicji taki sam typ, jak sama funkcja, dlatego wywolujemy inc (length t), a nie inc (length [a] t).

Porownywanie

Dowolne dwie wartosci tego samego (nie funkcyjnego) typu mozna porownac. Nieformalnie rzecz biorac typ jest nie funkcyjny gdy zadna jego wartosc nie moze skladac sie z zadnej funkcji. Na typach wbudowanych porzadek jest zgodny z intuicja (2 < 3, 'a' < 'b', ...), produkt ma porzadek po wspolrzednych, wartosci algebraiczne sa porownywane najpierw ze wzgledu na konstruktor jaki utworzyl dana wartosc, a potem ze wzgledu na jego argumenty.

Ponadto w pewnych sytuacjach porownywane sa typy. Rownosc na typach prostych, to rownosc strukturalna, wiec:


type typ [a] = unit;;

// typ [int] = typ [bool]
Troche inaczej wyglada sprawa z typami algebraicznymi, gdyz dwa typy algebraiczne sa rowne wtw. gdy powstaly w tej samej definicji i WSZYSTKIE ich argumenty polimorficzne sa rowne, wiec:

type typ [a] =
| Nic : unit;;

// typ [int] != typ [bool]
// typ [int] = typ [int]

Imperatywnosc

Jako stricte imperatywne nalezy uznac wszystkie wbudowane funkcje wejscia-wyjscia.

Ponadto dostepna jest wartosc specjalna o nie funkcyjnym dzialaniu:


# raise [a] : string => a = FUN
- funkcja, ktora zaaplikowana do napisu, powoduje zakonczenie programu z informacja tekstowa o bledzie.

Przykladowe zastosowanie:


let head [a] =
fun l : list [a] ->
  match l with
  | Nil _ -> raise [a] "Taking head of empty list"
  | Cons (h, _) -> h;;

# head [a] : list [a] => a = FUN

raise jest funkcja polimorficzna, aby dawalo sie dobrze okreslic typ nastepujacego wyrazenia:


let funkcja_pusta =
fun _ : int ->
  raise [char] "Ha, ha, ha ta funkcja jest wszedzie nieokreslona";;

# funkcja_pusta : int => char = FUN  

Niektore wbudowane funkcje i operatory w pewnych sytuacjach powoduja wywolanie funkcji raise. Typowy przyklad takiej sytuacji, to dzielnie przez zero.

Lazy vs. eager evaluation

Aplikacja (czy tez ewaluacja) jest leniwa, gdy argumenty nie zostaja calkowicie wyliczone, przed aplikacja funkcji. Generalnie FunAlg ma ewaluacje zachlanna i wylicza wszystkie argumenty, ale: W razie potrzeby mozna samodzielnie uleniwic dowolna funkcje:

let leniwa_fun =
fun x : unit => _typ_ =
  if chce_wyliczyc then
    let real_x = x () in
       ...
  else
    ...;;

let leniwa_appl = leniwa_fun (fun _ : unit -> x);;