2+3;;iar interpretorul răspunde:
- : int = 5Aici și în cele ce urmează, când dăm astfel de exemple, # e prompterul interpretorului (nu trebuie introdus), iar rândurile cu - : reprezintă răspunsul dat de interpretor.
Semnul ;; (dublu punct-virgulă) trebuie introdus pentru a indica sfărșitul expresiei de evaluat. Răspunsul (de după semnul :) indică rezultatul, 5, și tipul acestuia, întreg: int . Spațiile în cadrul expresiei nu contează, putem scrie și
# 2 + 3 ;;Cele două caractere ;; trebuie scrise însă unul după celălalt pentru fi interpretate ca sfârșit al textului de interpretat.
let f x = x + 3Cuvântul cheie let (engl. "fie") introduce o definiție, aici, pentru identificatorul f. Scrierea amintește de exprimarea: fie f dat de expresia ...
val f : int -> int = <fun>Interpretorul indică faptul că a fost definit identificatorul f cu o valoare care e o funcție. La stânga săgeții -> e domeniul de definiție al funcției, int, și la dreapta săgeții domeniul de valori, tot int.
Odată definită funcția, ea poate fi folosită (apelată) într-o expresie. Nici aici nu sunt necesare parantezele în jurul argumentului, decât dacă acesta este o expresie complexă:
# f 1;; - : int = 4 # f(2+3);; - : int = 8
# (fun x -> x + 3) 2;; - : int = 5fără a fi nevoie să dăm întâi un nume funcției. Acesta exemplu simplu ilustrează că în limbajul funcțional ML, o funcție (aici fun x -> x + 3) poate fi folosită la fel de simplu ca și orice altă valoare.
Revenind la notația fun, este echivalent să definim let f x = x + 3 sau
# let f = fun x -> x + 3;; val f : int -> int = <fun>
Când scriem un fișier program ML, nu trebuie să încheiem fiecare definiție cu ;; ca în interpretor, unde prin aceasta îi indicăm să evalueze fragmentul scris până acum. E nevoie de ;; doar când altfel nu ar fi clar unde se termină o definiție sau expresie și începe următoarea: după un șir de definiții urmat de o evaluare de expresie, și pentru a separa evaluări de expresii. De exemplu:
let f x = x + 2 let g x = x - 3 ;; f 2;; g 1
Pentru a încărca apoi fișierul creat în interpretorul OCaml, în Emacs se poate instala un pachet special, tuareg-mode. Acesta reprezintă programele ML colorate după regulile de sintaxă, și deci mai ușor de citit. Pachetul are însă și comenzi care permit rularea interpretorului Ocaml direct în editor.
În Emacs, putem lucra astfel în două sub-ferestre: una în care scriem codul ML,
și alta în care se rulează interpretorul OCaml. Secvențele de taste uzual folosite pentru a da comenzi în acest mod încep cu Ctrl-c ('c' de la 'compilare').
Cel mai simplu e să evaluăm definițiile (de funcții) pe rând, pe măsură ce le scriem. După ce am scris de exemplu let f x = x + 2, cu comanda Ctrl-c Ctrl-e ('e' = evaluare) se evaluează definiția din dreptul cursorului (acesta poate fi oriunde în cadrul sau după definiție). La prima execuție a acestei comenzi, în linia de jos a editorului (pentru comenzi și mesaje) apare mesajul OCaml toplevel to run: ocaml. Confirmăm cu tasta Enter că dorim să rulăm interpretorul. În jumătatea de jos a ecranului se indică faptul că definiția a fost evaluată: val f : int -> int = <fun> și apare prompterul # al interpretorului, la care putem evalua expresii, de exemplu f 3;;.
Comenzi utile:
Pentru a evita greșeli nedorite prin amestecarea definițiilor cu expresiile de evaluat, puteți lucra într-unul din două feluri:
Modul 1) Doar definițiile se scriu în fișierul ML:
let f x = x + 1 let g x = x + 2Toate evaluările de expresii/funcții se fac în porțiunea de jos cu interpretorul:
# f 3;; - : int = 4 # g 5 - : int = 7
Modul 2) Și testele se scriu în fișier, dându-le nume corespunzătoare:
let f x = x + 1 let test1 = f 3 let g x = x + 2 let test2 = g 5În felul acesta păstrăm și testele scrise și nu e nevoie deloc de ;; .
2 e o valoare întreagă, pentru o valoare reală trebuie să scriem 2.0 (sau prescurtat, 2.). La fel, operatorii + - * / lucrează doar cu întregi; pentru reali trebuie scris +. -. *. /. (toți operatorii sunt urmați de punct). ML nu face conversii implicite între întregi și reali; pentru a transforma un întreg în real folosim funcția float_of_int (sau echivalent, mai scurt, float).
# float (3 * 2);; - : float = 6.
Există și funcția printf, folosită asemănător ca în C, cu aceeași
convenție că argumentele se scriu separat, nu cu virgulă:
printf "intreg: %d real: %f\n" 3 1.2 va tipări
"intreg: 3 real: 1.2"
Pentru a folosi funcția printf trebuie însă înainte fie să deschidem modulul de bibliotecă Printf în care e definită:
open Printf
fie să folosim numele complet, având ca prefix numele modulului: Printf.printf "salut!"
open Printf let abs_print x = if x > 0 then ( printf "argument pozitiv: %d\n" x; x ) else ( printf "argument negativ: %d\n" x; -x )Exercițiul 1: minim/maxim (discutat la curs) Scrieți o funcție care returnează minimul/maximul a trei valori date ca parametri. Folosiți funcțiile predefinite min respectiv max care funcționează cu orice valori de același tip. Remarcați tipul funcției scrise și verificați că funcționează și cu întregi și cu reali (și chiar cu șiruri), însă nu cu un amestec.
Exercițiul 2: ecuația de gradul 2 Scrieți o funcție care ia ca parametri trei întregi a, b, c și tipărește soluțiile ecuației de gradul doi ax2+bx+c=0, sau un mesaj dacă nu există soluții reale. Folosiți funcția predefinită sqrt : float -> float pentru rădăcina pătrată și nu uitați conversiile de la întreg la real unde sunt necesare. Folosiți secvențierea când trebuie tipărite două soluții.
Exercițiul 3: an bisect
Scrieți o funcție care determină dacă un an (întreg) dat ca parametru e bisect, returnând un boolean. Dacă un an e bisect sau nu se poate determina după următoarele reguli (va trebui sa le reformulați sau reordonați pentru a scrie funcția):
a) un an divizibil la 4 e bisect, altfel nu
b) prin excepție de la a), anii divizibili cu 100 nu sunt bisecți
c) prin excepție de la b), anii divizibili cu 400 sunt bisecți
în OCaml putem defini o funcție de compoziție:
# let comp f g x = f (g x);; val comp : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b = <fun>Vedem că tipul funcției comp este mai general decât cele întâlnite până acum. Funcția comp ia ca parametru o funcție (f) cu tipul 'a -> 'b și apoi o altă funcție (g) de tipul 'c -> 'a și produce o funcție (f °g) cu tipul 'c -> 'b . Aceste tipuri corespund cu cele din definiția matematică de mai sus, și reflectă faptul că domeniul de valori A al lui g trebuie să fie același cu domeniul de definiție al lui f . În OCaml, 'a, 'b, 'c sunt variabile de tip, care pot fi instanțiate (înlocuite) cu orice tip concret din limbaj (int, float, etc.).
Spunem că funcția comp este polimorfă (poate lua mai multe forme, în funcție de tipurile concrete substituite pentru 'a, 'b, 'c . Polimorfismul ne permite să scriem cod cât mai general, și e o noțiune deosebit de importantă care stă și la baza programării orientate pe obiecte.
# let appl2 f = comp f f;; val appl2 : ('a -> 'a) -> 'a -> 'a = <fun>În același fel, putem compune funcția appl2 cu ea însăși, obținând o funcție care aplică de 4 ori funcția primită ca parametru:
# let appl4 = comp appl2 appl2;; val appl4 : ('_a -> '_a) -> '_a -> '_a = <fun>Putem defini însă și altă funcție:
# let appl4p = appl2 appl2;; val appl4p : ('_a -> '_a) -> '_a -> '_a = <fun>primul appl2 produce aplicarea de două ori a funcției argument, care e tot appl2 . Putem urmări rezultatul folosind o funcție exemplu:
# let f x = 2 * x + 1;; val f : int -> int = <fun> # appl4 f 0;; - : int = 15 # appl4p f 0;; - : int = 15Încercând cu mai multe funcții și argumente întregi, obținem valori egale, sugerând că în cele două variante am definit de fapt aceeași funcție. Puteți explica de ce?
Varianta cea mai uzuală de a transcrie această funcție într-un program ML este:
# let iadd x y = x + y;; val iadd : int -> int -> int = <fun>Observăm că în tipul funcției, int -> int -> int, săgeata -> apare de două ori, pe cănd în exemplul dinainte apărea o singură dată, în stânga fiind tipul pentru domeniul de definiție, iar în dreapta tipul pentru domeniul de valori.
De fapt, suntem în același caz general: considerănd
prima săgeată, la stânga ei e domeniul de
definiție al funcției iadd, int, iar
în dreapta ei, tipul domeniului de valori, int ->
int. Deci, așa cum a fost scrisă,
funcția f are un parametru întreg, iar
rezultatul este el însuși o funcție, care ia ca
parametru un întreg și returnează un întreg.
O astfel de funcție care returnează o funcție sau
ia o funcție ca parametru se numește funcție de
ordin superior (engl. higher-order function) sau
funcțională. Ele sunt un lucru foarte natural în
limbajele funcționale, deoarece acestea pot lucra cu
funcții ca și cu orice alt tip de valori.
Apelând funcția iadd cu un argument pentru parametrul x, rezultatul iadd x este o funcție de un singur argument y, și anume fun y -> x + y, în care x nu mai e parametru, ci o valoare cunoscută (legată) furnizată deja (la apelul iadd x).
(+) 2 3;; - : int = 5E nevoie de paranteze pentru (+), altfel expresia +2 3 ar fi incorectă (două valori alăturate, ca un apel de funcție, dar +2 e un număr, nu o funcție).
# (+) ;; - : int -> int -> int = <fun>acesta ne spune că (+) e o funcție care ia un parametru int și produce ca rezultat o funcție (int -> int), care la răndul ei ia un parametru întreg și produce un rezultat întreg. De fapt, funcția (+) este identică cu funcția iadd definită înainte. Putem scrie:
# let f = (+) 3;; val f : int -> int = <fun> # f 2;; - : int = 5 # ((+) 2) 3;; - : int = 5
Exercițiul 4: valori distincte
Scrieți o funcție cu trei parametri (de același tip oarecare), care returnează câte valori distincte există între argumentele primite (unul, două sau trei) și tipărește, după caz, un mesaj: "toate argumentele sunt distincte/egale" sau "argumentele 1 și 2 (resp. 2 și 3, sau 1 și 3) sunt egale". Evitați pe cât posibil duplicarea de cod: pentru porțiuni de cod similare, creați (și apelați) o funcție care conține partea comună și are ca parametri valorile care diferă.
Exercițiul 5: mediana
Scrieți o funcție care calculează mediana a trei valori (valoarea aflată între celelalte două).
Încercați să scrieți cod cât mai simplu, și să nu-l repetați. Puteți folosi o funcție auxiliară care calculează mediana a trei numere, pentru care știm că primul e mai mic sau egal decât al doilea. Sau puteți încerca să compuneți doar funcțiile standard max/min de două elemente (expresia trebuie să fie oarecum simetrică). Care din variante necesită mai puține comparații?
Exercițiul 6: operații cu funcții
În matematică, am extins uneori operatorul + de la numere la funcții,
defining funcția f + g prin relația (f + g)(x) = f(x) + g(x)
a) Definiți în ML o funcție care ia ca parametru două funcții f și g și
returnează funcția definită ca suma lor prin relația de mai sus.
b) Scrieți o funcție mai generală, care primește ca parametru și
operatorul binar (o funcție de două argumente) care e aplicată (valorilor)
celor două funcții. Verificați că o puteți folosi cu operatorul (+) și
( *) pentru a calcula suma și produsul.
În scrierea operatorului ( *) trebuie un spațiu între ( și * pentru a nu fi interpretat ca și comentariu. În ML, comentariile sunt încadrate între (* și *).
Exercițiu 7: tarife cu bonus
În New York, o călătorie cu transportul în comun costă $2.75.
La încărcarea cardului de transport se acceptă doar sume în multipli de 5
cenți. Pentru încărcarea cu valoarea a cel puțin două călătorii se oferă un bonus de 5%, rotunjit la cent. Scrieți o funcție care calculează și returnează suma minimă care trebuie incărcată pentru N călătorii, și afișează restul care rămâne pe card. Puteți verifica rezultatele aici.