If, loop e ricorsione
Istruzioni if (sono in realtà espressioni if)
OCaml ha un'istruzione if con due variazioni, e l'ovvio significato:
if condizione-booleana then espressione
if condizione-booleana then espressione else altra-espressione
Diversamente che nei linguaggi convenzionali a cui sarete abituati, le
istruzioni if sono in realtà espressioni. In altre parole, sono molto
più come condizione-booleana ? espressione : altra-espressione
che
come le istruzioni if a cui potreste essere abituati.
Ecco un semplice esempio di istruzione if:
# let max a b =
if a > b then a else b;;
val max : 'a -> 'a -> 'a = <fun>
Per inciso, se digitate questo nel toplevel di OCaml, noterete che OCaml decide che questa funzione è polimorfica, con il seguente tipo:
max : 'a -> 'a -> 'a
Ed appunto OCaml vi lascia utilizzare max
su qualunque tipo:
# max 3 5;;
- : int = 5
# max 3.5 13.0;;
- : float = 13.
# max "a" "b";;
- : string = "b"
Questo perché >
è di fatto polimorfico. Funziona su qualunque tipo,
anche su oggetti (fa un confronto binario).
[Si noti che il modulo `Pervasives` definisce `min` e `max` per il programmatore.]
Osserviamo un po' più da vicino l'espressione if. Segue la funzione
range
che vi ho mostrato in precedenza senza tante spiegazioni.
Dovreste poter combinare la vostra conoscenza delle funzioni ricorsive,
delle liste e delle espressioni if per vedere che cosa fa:
# let rec range a b =
if a > b then []
else a :: range (a+1) b;;
val range : int -> int -> int list = <fun>
Esaminiamo alcune chiamate tipiche a questa funzione. Cominciamo con il
caso facile di a > b
. Una chiamata a range 11 10
restituisce []
(la lista vuota) e questo è tutto.
E la chiamata a range 10 10
? Visto che 10 > 10
è falso, è valutata
la clausola else
, che è: 10 :: (range 11 10)
(Ho aggiunto le
parentesi per rendere più chiaro l'ordine in cui le espressioni sono
valutate). Abbiamo appena ricavato che è range 11 10
= []
, per cui
questo è: 10 :: []
. Ricordate la nostra descrizione formale delle
liste e l'operatore ::
(cons)? 10 :: []
è esattamente lo stesso che
[ 10 ]
.
Proviamo range 9 10
:
range 9 10
=> 9 :: (range 10 10)
=> 9 :: [ 10 ]
=> [9; 10]
Dovrebbe essere discretamente chiaro che range 1 10
è valutato a
[ 1; 2; 3; 4; 5; 6; 7; 8; 9; 10 ]
.
Quello che abbiamo qui è un semplice caso di ricorsione. La programmazione funzionale si può dire preferisca la ricorsione ai cicli, ma mi sto superando. Discuteremo meglio la ricorsione alla fine di questo capitolo.
Torniamo per un momento alle istruzioni if. Che cosa fa questa funzione?
# let f x y =
x + if y > 0 then y else 0;;
val f : int -> int -> int = <fun>
Indizio: aggiungete delle parentesi intorno all'intera espressione if.
Unisce y
come un diodo
elettronico.
La funzione abs
(valore assoluto) è definita in Pervasives
come:
let abs x =
if x >= 0 then x else -x
Sempre in Pervasives
, la funzione string_of_float
contiene un
complesso paio di espressioni if annidate:
let string_of_float f =
let s = format_float "%.12g" f in
let l = string_length s in
let rec loop i =
if i >= l then s ^ "."
else if s.[i] = '.' || s.[i] = 'e' then s
else loop (i+1)
in
loop 0
Esaminiamo questa funzione. Si supponga che la funzione sia chiamata con
f
= 12.34. Allora s
= "12.34", e l
= 5. Chiamiamo loop
la prima
volta con i
= 0.
i
non è maggiore o uguale a l
, e s.[i]
(il carattere i
^th^ in
s
) non è un punto o una 'e'
. Così è chiamata loop (i+1)
, p.e.
loop 1
.
Passiamo per la stessa danza per i
= 1, e finiamo chiamando loop 2
.
Per i
= 2, tuttavia, s.[i]
è un punto (fate riferimento alla stringa
originale, s
= "12.34"). Così questa restituisce immediatamente s
, e
la funzione string_of_float
restituisce "12.34".
Che cosa sta facendo loop
? Di fatto sta verificando se la stringa
restituita da format_float
contiene un punto (o 'e'
). Si supponga
che abbiamo chiamato string_of_float
con 12.0
. format_float
restituirebbe la stringa "12", ma string_of_float
deve restituire
"12." o "12.0" (poiché le costanti floating point in OCaml devono
contenere un punto affinché si possano distinguere dalle costanti
intere). Di qui il controllo.
Lo strano uso della ricorsione in questa funzione serve quasi certamente
all'efficienza. OCaml supporta i loop, dunque perché gli autori non
hanno usato loop for? Vedremo nella prossima sezione che i loop for di
OCaml sono limitati in una maniera che impedisce che siano usati in
string_of_float
. Ecco tuttavia un modo più lineare, ma
approssimativamente lento il doppio, di scrivere string_of_float
:
let string_of_float f =
let s = format_float "%.12g" f in
if String.contains s '.' || String.contains s 'e' then s
else s ^ "."
Usare begin ... end
Ecco del codice da lablgtk:
if GtkBase.Object.is_a obj cls then
fun _ -> f obj
else begin
eprintf "Glade-warning: %s expects a %s argument.\n" name cls;
raise Not_found
end
begin
ed end
sono ciò che è noto nel caso delle parentesi aperte e
chiuse come zucchero sintattico. Nell'esempio sopra, tutto ciò che
essi fanno è raggruppare insiemele due istruzioni nella clausola else
.
Supponete che l'autore avesse invece scritto questo:
if GtkBase.Object.is_a obj cls then
fun _ -> f obj
else
eprintf "Glade-warning: %s expects a %s argument.\n" name cls;
raise Not_found
Un completo uso delle parentesi ed un'appropriata indentazione dell'espressione sopra dànno:
(if GtkBase.Object.is_a obj cls then
fun _ -> f obj
else
eprintf "Glade-warning: %s expects a %s argument.\n" name cls
);
raise Not_found
Nient'affatto ciò che si intendeva. Così le parole begin
e end
sono
necessarie per raggruppare insieme istruzioni multiple in una clausola
then
o else
di un'espressione if. Potete anche usare le parentesi
tonde ordinarie ( ... )
se preferite (ed io lo preferisco, perché
detesto Pascal :-). ecco due semplici esempi:
# if 1 = 0 then
print_endline "THEN"
else begin
print_endline "ELSE";
failwith "else clause"
end;;
ELSE
Exception: Failure "else clause".
# if 1 = 0 then
print_endline "THEN"
else (
print_endline "ELSE";
failwith "else clause"
);;
ELSE
Exception: Failure "else clause".
Cicli for e cicli while
OCaml supporta una forma piuttosto limitata del familiare ciclo for:
for variable = start to end do
expression
done
for variable = start downto end do
expression
done
Un esempio semplice ma reale da lablgtk:
for i = 1 to n_jobs () do
do_next_job ()
done
In OCaml i loop for sono semplicemente scorciatoie per scrivere:
let i = 1 in
do_next_job ();
let i = 2 in
do_next_job ();
let i = 3 in
do_next_job ();
... & co. ...
let i = n_jobs () in
do_next_job ();
OCaml non supporta il concetto di uscita anticipata da un loop (in realtà, potreste sollevare un'eccezione e farlo uscire, ma ciò sarebbe lento e rozzo). I programmatori funzionali tendono ad utilizzare la ricorsione al posto dei loop espliciti, e guardano con sospetto ai cicli for, donde il motivo per cui il loop for di OCaml è relativamente impotente. Parliamo della ricorsione più sotto.
L'espressione all'interno di un loop for di OCaml dovrebbe essere
valutata a unit
(altrimenti otterreste un'avviso), e l'intera
espressione del loop for restituisce unit
:
# for i = 1 to 10 do i done;;
Warning 10: this expression should have type unit.
- : unit = ()
I cicli while in OCaml sono scritti:
while condizione-booleana do
espressione
done
Come per i cicli for, non v'è alcun modo fornito dal linguaggio di uscire in anticipo da un ciclo while, eccetto il sollevare un'eccezione, e ciò significa che i cicli while hanno un'utilizzo abbastanza limitato. Ancora, ricordate che ai programmatori funzionali piace la ricorsione, e quindi i cicli while sono cittadini di seconda classe nel linguaggio.
Se smettete di considerare i cicli while, potreste vedere che non hanno alcuna reale utilità se non in congiunzione con i nostri vecchi amici riferimenti. Immaginiamo per un momento che OCaml non abbia i riferimenti:
let quit_loop = false in
while not quit_loop do
print_string "Have you had enough yet? (y/n) ";
let str = read_line () in
if str.[0] = 'y' then
(* come do a quit_loop valore true ?!? *)
done
Ricordate che quit_loop
non è una vera "variabile" - essa soltanto
rende quit_loop
un alias per false
. Ciò significa che l'intera
condizione while (mostrata in rosso) è sempre vera, e che il loop va
avanti all'infinito!
Fortunatamente OCaml ha i riferimenti, così che possiamo scrivere il
codice sopra se vogliamo. Non confondetevi pensando che il !
(punto
esclamativo) significhi "non" come in C/Java. È utilizzato qui con il
significato "dereferenzia il puntatore", in modo simile di fatto a
quanto avviene in Forth. Fate meglio a leggere !
come "get" o "deref".
let quit_loop = ref false in
while not !quit_loop do
print_string "Have you had enough yet? (y/n) ";
let str = read_line () in
if str.[0] = 'y' then
quit_loop := true
done
Eseguire loop sulle liste
Se volete ciclare su una lista, non fate i programmatori imperativi e
non cercate di raggiungere la vostra fidata rivoltella Mr. For Loop!
OCaml ha dei modi migliori e più veloci di eseguire loop sulle liste, e
sono tutti collocati nel modulo List
. Vi sono infatti decine di buone
funzioni in List
, ma qui parlerò soltanto di quelle più utili.
Per prima cosa, definiamo una lista da usare:
# let my_list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10];;
val my_list : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
Se volete chiamare una funzione una volta su ciascun elemento della
lista, usate List.iter
, così:
# let f elem =
Printf.printf "I'm looking at element %d now\n" elem in
List.iter f my_list;;
I'm looking at element 1 now
I'm looking at element 2 now
I'm looking at element 3 now
I'm looking at element 4 now
I'm looking at element 5 now
I'm looking at element 6 now
I'm looking at element 7 now
I'm looking at element 8 now
I'm looking at element 9 now
I'm looking at element 10 now
- : unit = ()
List.iter
è in effetti ciò che dovreste pensare in primo luogo di
utilizzare ogni volta che il vostro cervello vi suggerisce di usare un
ciclo for.
Se volete trasformare ciascun elemento separatamente nella lista - per
esempio, duplicare ciascun elemento della lista - allora usate
List.map
. Questa funzione sarà familiare a chi ha già programmato in
Perl.
# List.map (( * ) 2) my_list;;
- : int list = [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]
Perl ha l'utile funzione "grep" per filtrare soltanto gli elementi di
una lista che soddisfano qualche condizione - p.e. restituire soltanto i
numeri pari di una lista. In OCaml questa funzione è chiamata
List.filter
:
# let is_even i =
(i mod 2) = 0 in
List.filter is_even my_list;;
- : int list = [2; 4; 6; 8; 10]
Per trovare se una lista contiene particolari elementi, usate List.mem
(abbreviazione per member):
# List.mem 12 my_list;;
- : bool = false
List.for_all
e List.exists
sono lo stesso degli operatori "forall"
ed "exist" nella logica dei predicati (rispettivamente, "per ogni" ed
"esiste", NdT).
Per operare su due liste allo stesso tempo, vi sono varianti "-2" di
alcune di queste funzioni, vale a dire iter2
, map2
, for_all2
,
exists2
.
Le funzioni map
e filter
operano su singoli elementi di liste
isolati. Fold è un'operazione più insolita a cui si può
pensare nel modo migliore come all'"inserimento di un operatore a
separare ciascun elemento della lista". Supponete che volessi sommare
insieme tutti gli elementi della mia lista. In termini eloquenti ciò che
voglio fare è inserire un segno più fra gli elementi della mia lista:
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10
L'operazione fold fa questo, sebbene i dettagli esatti siano un pochino
più complicati. Prima di tutto, che cosa accade se tento di eseguire
fold su una lista vuota? Nel caso della somma sarebbe carino se il
risultato fosse zero, invece che un errore. Tuttavia se tentasssi di
trovare il prodotto della lista preferirei che la risposta fosse invece
uno. Devo dunque ovviamente fornire una qualche sorta di argomento di
"default" al mio fold. Il secondo problema non si presenta con semplici
operatori come +
e *
: che cosa accade se l'operatore che sto
utilizzando non è associativo, cioè (a op b) op c non è uguale a
op (b op c)? In tal caso importerebbe se partissi da sinistra e
operassi verso destra, piuttosto che se partissi da destra e operassi
verso sinistra. Per questo motivo vi sono due versioni di fold, chiamate
List.fold_left
(lavora da sinistra verso destra) e List.fold_right
(lavora da destra verso sinistra, ed è anche meno efficiente).
Usiamo List.fold_left
per definire le funzioni sum
e product
per
liste di interi:
# let sum = List.fold_left ( + ) 0;;
val sum : int list -> int = <fun>
# let product = List.fold_left ( * ) 1;;
val product : int list -> int = <fun>
# sum my_list;;
- : int = 55
# product my_list;;
- : int = 3628800
È stato facile! Notate che ho trovato per caso un modo di fare fattoriali matematici:
# let fact n = product (range 1 n);;
val fact : int -> int = <fun>
# fact 10;;
- : int = 3628800
(Notate che questa funzione fattoriale non è molto utile poiché compie
overflow sugli interi e dà risposte errate anche per valori abbastanza
piccoli di n
. Una funzione fattoriale reale utilizzerebbe il modulo
Big_int
.)
Eseguire loop sulle stringhe
Il modulo String
contiene anche parecchie dozzine di utili funzioni
relative alle stringhe, ed alcune di esse hanno a che fare con
l'esecuzione di loop su stringhe.
String.fill
e String.blit
sono gli equivalenti rispettivamente di
memset
e strcpy
di C. String.copy
copia una stringa, come
strdup
.
Vi è anche una funzione String.iter
che funziona come List.iter
, ma
sui caratteri della stringa.
Ricorsione
Veniamo ora ad un argomento importante - la ricorsione. I programmatori funzionali sono definiti in base al loro amore per le funzioni ricorsive, ed in molti modi le funzioni ricorsive nella programmazione funzionale sono l'equivalente dei loop nella programmazione imperativa. Nei linguaggi funzionali i loop sono cittadini di seconda classe, mentre le funzioni ricorsive hanno tutte il migliore supporto.
Scrivere funzioni ricorsive necessita di un cambio di impostazione mentale rispetto allo scrivere cicli for e cicli while. Sicché ciò che vi darò in questa sezione sarà semplicemente un'introduzione e degli esempi.
Nel primo esempio leggeremo in memoria un intero file (in una lunga stringa). Per questo vi sono essenzialmente tre possibili approcci:
Approccio 1
Prendi la lunghezza di un file e leggilo tutto in una volta usando il
metodo really_input
. Questo è il più semplice, ma potrebbe non
funzionare su canali che non sono realmente file (p.e. lettura di input
da tastiera), motivo per cui guardiamo gli altri due approcci.
Approccio 2
L'approccio imperativo, che utilizza un ciclo while da cui si esce usando un'eccezione.
Approccio 3
Un loop ricorsico, che esce dalla ricorsione ancora usando un'eccezione.
Introdurremo qui alcuni concetti nuovi. I nostri secondi due approcci
utilizzeranno il modulo Buffer
- un buffer espandibile che potete
pensare come una stringa nella quale potete efficacemente aggiungere
altro testo in coda. Intercetteremo inoltre l'eccezione End_of_file
che le funzioni di input sollevano quando raggiungono la fine
dell'input. Utilizzeremo anche Sys.argv.(1)
per prendere il primo
parametro dalla linea di comando.
(* Leggi l'intero file: Approccio 1 *)
let read_whole_chan chan =
let len = in_channel_length chan in
let result = String.create len in
really_input chan result 0 len;
result
let read_whole_file filename =
let chan = open_in filename in
read_whole_chan chan
let filename = Sys.argv.(1)
let str = read_whole_file filename
let () =
Printf.printf "I read %d characters from %s\n"
(String.length str) filename
L'approccio 1 funziona ma non è molto soddisfacente poiché
read_whole_chan
non funziona su canali diversi da file come input da
tastiera o socket. L'approccio 2 coinvolge un ciclo while:
(* Leggi l'intero file: Approccio 2 *)
let read_whole_chan chan =
let buf = Buffer.create 4096 in
try
while true do
let line = input_line chan in
Buffer.add_string buf line;
Buffer.add_char buf '\n'
done;
""
with
End_of_file -> Buffer.contents buf
let read_whole_file filename =
let chan = open_in filename in
read_whole_chan chan
let filename = Sys.argv.(1)
let str = read_whole_file filename
let () =
Printf.printf "I read %d characters from %s\n"
(String.length str) filename
La chiave all'approccio 2 è quella di guardare il ciclo while centrale.
Ricordate che ho detto che il solo modo di uscire da un ciclois to look
at the central while in anticipo è con un'eccezione? Questo è
esattamente ciò che facciamo qui. Sebbene io non abbia ancora coperto le
eccezioni, probabilmente non avrete alcun problema a comprendere
l'eccezione End_of_file
sollevata nel codice sopra da input_line
quando essa incontra la fine del file. Il buffer buf
accumula i
contenuti del file, e quando incontriamo la fine del file la restituiamo
(Buffer.contents buf
).
Un punto curioso riguardo ciò è che l'insieme extra apparentemente
superfluo di virgolette (""
) subito dopo il ciclo while. A che
servono? Ricordate che i cicli while, come i cicli for, sono
semplicemente espressioni, e che restituiscono l'oggetto unit
(()
).
Tuttavia OCaml richiede che il tipo restituito all'interno di un try
corrisponda con il tipo ritornato di ciascun'eccezione intercettata. In
questo caso, poiché End_of_file
risulta in un string
, il corpo
principale del try
deve anch'esso "restituire" una stringa - anche se
a causa del ciclo while infinito la stringa non potrebbe mai essere
effettivamente restituita.
Ecco la nostra versione ricorsiva. Notate che è più breve dell'approccio 2, ma non così facile da capire almeno per i programmatori imperativi:
(* Leggi l'intero file: Approccio 3 *)
let read_whole_chan chan =
let buf = Buffer.create 4096 in
let rec loop () =
let line = input_line chan in
Buffer.add_string buf line;
Buffer.add_char buf '\n';
loop ()
in
try
loop ()
with
End_of_file -> Buffer.contents buf
let read_whole_file filename =
let chan = open_in filename in
read_whole_chan chan
let filename = Sys.argv.(1)
let str = read_whole_file filename
let () =
Printf.printf "I read %d characters from %s\n"
(String.length str) filename
Abbiamo ancora un ciclo infinito - ma in questo caso fatto usando la
ricorsione. loop
chiama se stessa alla fine della funzione. La
ricorsione infinita è interrotta quando input_line
solleva
un'eccezione End_of_file
.
Sembrerebbe che l'approccio 3 potrebbe causare un overflow dello stack
se gli date un file particolarmente grosso, ma di fatto non è questo il
caso. A causa della ricorsione a coda (discussa sotto) il compilatore
trasformerà la funzione ricorsiva loop
in un vero ciclo while (!) che
è eseguito in uno spazio di stack costante.
Nel prossimo esempio mostreremo come la ricorsione è eccellente per costruire o esaminare alcuni tipi di strutture dati, in paricolare gli alberi. Facciamoci un tipo ricorsivo per rappresentare i file in un filesystem:
type filesystem = File of string | Directory of filesystem list;;
Le funzioni opendir
e readdir
sono utilizzate per aprire una
directory e leggere gli elementi dalla directory. Definirò una pratica
funzione readdir_no_ex
che nasconde la fastidiosa eccezione
End_of_file
che readdir
solleva quando raggiunge la fine della
directory:
open Unix (* Potreste dover inserire prima #load "Unix.cma" *)
let readdir_no_ex dirh =
try
Some (readdir dirh)
with
End_of_file -> None
Il tipo di readdir_no_ex
è quello sotto. Riconsiderate la nostra
precedente discussione sui puntatori nulli.
readdir_no_ex : dir_handle -> string option
Definirò inoltre una semplice funzione ricorsiva che potrò utilizzare
per convertire il tipo filesystem
in una stringa per stampare (p.e.):
let rec string_of_filesystem fs =
match fs with
| File filename -> filename ^ "\n"
| Directory fs_list ->
List.fold_left (^) "" (List.map string_of_filesystem fs_list)
Si noti l'uso di fold_left
e di map
. La funzione map
è usata per
convertire (ricorsivamente) ciascun filesystem
nella lista in un
string
. Quindi fold_left (^) ""
concatena insieme la lista in
un'unica grande stringa. Notate anche l'utilizzo del pattern matching.
(La libreria definisce una funzione chiamata String.concat
che è
essenzialmente equivalente a fold_left (^) ""
, ma implementata in modo
più efficiente).
Definiamo ora una funzione che legga una struttura di una directory,
ricorsivamente, e restituisca una struttura dati ricorsiva filesystem
.
Mostrerò questa funzione passo per passo, ma stamperò l'intera funzione
alla fine di questa sezione. In primo luogo il profilo della funzione:
let rec read_directory path =
let dirh = opendir path in
let rec loop () =
(* ..... *)
in
Directory (loop ())
La chiamata a opendir
apre il percorso fornito e restituisce un
dir_handle
dal quale potremo poi leggere i nomi utilizzando
readdir_no_ex
. Il valore restituito della funzione sarà un
Directory fs_list
, dunque tutto ciò che dobbiamo fare per completare
la funzione è scrivere la nostra funzione loop
che restituisce una
lista di filesystem
. Il tipo di loop
sarà:
loop : unit -> filesystem list
Come definiamo dei loop? Vediamolo ancora per passi.
let rec loop () =
let filename = readdir_no_ex dirh in
(* ..... *)
Per prima cosa leggiamo il prossimo nome di file (filename) dall'handle
della directory. filename
ha tipo string option
, in altre parole
potrebbe essere None
o Some "foo"
, dove foo
è il nome del prossimo
filename nella directory. Dobbiamo anche ignorare i file "."
e ".."
(cioè la directory attuale e la directory madre). Possiamo tutti fare
questo con un bel pattern match:
let rec loop () =
let filename = readdir_no_ex dirh in
match filename with
| None -> []
| Some "." -> loop ()
| Some ".." -> loop ()
| Some filename ->
(* ..... *)
Il caso None
è facile. Pensando ricorsivamente (!) se è loop
è
chiamata, e abbiamo raggiunto la fine della directory, loop
deve
restituire una lista di voci - e non ci sono voci - sicché restituisce
la lista vuota ([]
).
Per "."
e ".."
ignoriamo semplicemente il file e chiamiamo ancora
loop
.
Che cosa facciamo quando loop
legge un vero nome di file (il match
Some filename
sotto)? Sia pathname
il percorso completo per il file.
Eseguiamo 'stat' sul file per vedere se è realmente una directory. Se
è una directory, impostiamo this
chiamando ricorsivamente
read_directory
che restituirà Directory qualcosa
. Notate che il
risultato complessivo di read_directory
è Directory (loop ())
. Se il
file è realmente un file (non una directory) facciamo che this
sia
File pathname
. Facciamo quindi qualcosa di intelligente: restituiamo
this :: loop ()
. Questa è la seconda chiamata ricorsiva a loop ()
per calcolare i rimanenti membri della directory (una lista), a cui
preponiamo this
.
let rec read_directory path =
let dirh = opendir path in
let rec loop () =
let filename = readdir_no_ex dirh in
match filename with
| None -> []
| Some "." -> loop ()
| Some ".." -> loop ()
| Some filename ->
let pathname = path ^ "/" ^ filename in
let stat = lstat pathname in
let this = if stat.st_kind = S_DIR then
read_directory pathname
else
File pathname in
this :: loop ()
in
Directory (loop ())
È un caso di ricorsione piuttosto complesso, ma sebbene questo sia un esempio ad arte esso è abbastanza tipico dei complessi pattern di ricorsione che si trovano nei programmi funzionali del mondo reale. Le due importanti lezioni da trarre da questo sono:
L'utilizzo della ricorsione per costruire una lista:
et rec loop () = un'istruzione match o if | caso base -> [] | caso ricorsivo -> elemento :: loop ()
Confrontate questo con la nostra precedente funzione
range
. Il pattern di ricorsione è esattamente il medesimo:let rec range a b = if a > b then [] (* Caso base *) else a :: range (a+1) b (* Caso ricorsivo *)
L'utilizzo della ricorsione per costruire alberi:
let rec read_directory path = (* bla bla *) if file-is-a-directory then read_directory path-to-file else Leaf file
Tutto ciò che rimane ora per rendere questo un programma funzionante è
un poco di codice per chiamare read_directory
e mostrare il risultato:
let path = Sys.argv.(1)
let fs = read_directory path
let () = print_endline (string_of_filesystem fs);;
Addendum
Dopo aver postato questo esempio sulla mailing list caml-list ho ricevuto molte repliche. (Potete leggere l'intero thread.)
First of all there is a basic fault with
read_directory
directory which will cause it to fail if you try to run it on very large directory structures. I don't explicitly callclosedir
to close the directory handle. The garbage collector doesn't help, because in OCaml the garbage collector does not collect and close open file and directory handles.The fix is pretty trivial: add a call to
closedir
at the end of theread_directory
function:let rec read_directory path = let dirh = opendir path in (* ... *) let result = Directory (loop ()) in closedir dirh; result
Next up is the issue of
readdir
throwing an exception in a rather "unexceptional" situation, namelyEnd_of_file
. I don't agree that throwing an exception in this case is the right thing to do. In typical programs I want to never have to write atry ... with
clause, because exceptions are supposed to mean "something really bad happened which I'm not prepared to deal with", like running out of disk space or memory. Throwing an exception as part of the routine running of a function (every program which usesreaddir
has to be prepared to handleEnd_of_file
), that's not an exceptional situation.However, Eric Cooper came up with a way to write the function and check the exception, and this highlights another frequent technique used by functional programmers. Namely, passing an accumulator which contains the result of the function call, but passed as an argument to the function (!) Really the accumulator means "this is the result so far", so in the exceptional case (
End_of_file
) we give up and return the result so far. Here is his code with all the references to the accumulator argument in red:let rec read_filesystem path = if (lstat path).st_kind = S_DIR then Directory (read_directory path) else File path and read_directory path = let dirh = opendir path in let rec loop entries = try match readdir dirh with | "." | ".." -> loop entries | filename -> loop (read_filesystem (path ^ "/" ^ filename) :: entries) with End_of_file -> entries in let list = loop [] in closedir dirh; list
Notice
End_of_file -> entries
which means "when we get the exception, give up and return the result so far".Next up - it was pointed out that the easiest way to do this is actually not to use recursion at all, but instead to do the loop imperatively (a while loop is probably best) and have a reference to a list to store the ongoing accumulated result. Because
readdir
throws an exception, we have a simple way to break out of the while loop, and in thewith
-clause we can just return!list
(dereference the reference to the list of entries we've built up).It all works nicely, and proves that writing code imperatively is often best, particularly as in this case where
opendir ... readdir ... closedir
is essentially an imperative API, designed for C programmers. It's no surprise, therefore, that the imperative solution is easier. OCaml, not being a bondage and discipline language, doesn't mind you using the imperative approach when it makes sense.Here is the outline of the imperative approach by Fabrice Le Fessant:
let list = ref [] in let dir = opendir "......." in try while true do match readdir dir with | "." | ".." -> () | filename -> list := filename :: !list done with End_of_file -> !list
Recursion example: maximum element in a list
Remember the basic recursion pattern for lists:
let rec loop () = a match or if statement | base case -> [] | recursive case -> element :: loop ()
The key here is actually the use of the match / base case / recursive case pattern. In this example - finding the maximum element in a list - we're going to have two base cases and one recursive case. But before I jump ahead to the code, let's just step back and think about the problem. By thinking about the problem, the solution will appear "as if by magic" (I promise you :-)
First of all, let's be clear that the maximum element of a list is just the biggest one. eg. The maximum element of the list
[1; 2; 3; 4; 1]
is4
.Are there any special cases? Yes there are. What's the maximum element of the empty list
[]
? There isn't one. If we are passed an empty list, we should throw an error.What's the maximum element of a single element list such as
[4]
? That's easy: it's just the element itself. Solist_max [4]
should return4
, or in the general case,list_max [x]
should returnx
.What's the maximum element of the general list
x :: remainder
(this is the "cons" notation for the list, soremainder
is the tail - also a list)?Think about this for a while. Suppose you know the maximum element of
remainder
, which is, say,y
. What's the maximum element ofx :: remainder
? It depends on whetherx > y
orx <= y
. Ifx
is bigger thany
, then the overall maximum isx
, whereas conversely ifx
is less thany
, then the overall maximum isy
.Does this really work? Consider
[1; 2; 3; 4; 1]
again. This is1 :: [2; 3; 4; 1]
. Now the maximum element of the remainder,[2; 3; 4; 1]
, is4
. So now we're interested inx = 1
andy = 4
. That head elementx = 1
doesn't matter becausey = 4
is bigger, so the overall maximum of the whole list isy = 4
.Let's now code those rules above up, to get a working function:
# let rec list_max xs = match xs with | [] -> (* empty list: fail *) failwith "list_max called on empty list" | [x] -> (* single element list: return the element *) x | x :: remainder -> (* multiple element list: recursive case *) max x (list_max remainder);;
val list_max : 'a list -> 'a = <fun>I've added comments so you can see how the rules / special cases we decided upon above really correspond to lines of code.
Does it work?
# list_max [1; 2; 3; 4; 1];;
- : int = 4 # list_max [];;
Exception: Failure "list_max called on empty list". # list_max [5; 4; 3; 2; 1];;
- : int = 5 # list_max [5; 4; 3; 2; 1; 100];;
- : int = 100Notice how the solution proposed is both (a) very different from the imperative for-loop solution, and (b) much more closely tied to the problem specification. Functional programmers will tell you that this is because the functional style is much higher level than the imperative style, and therefore better and simpler. Whether you believe them is up to you. It's certainly true that it's much simpler to reason logically about the functional version, which is useful if you wanted to formally prove that
list_max
is correct ("correct" being the mathematical way to say that a program is provably bug-free, useful for space shuttles, nuclear power plants and higher quality software in general).Tail recursion
Let's look at the
range
function again for about the twentieth time:# let rec range a b = if a > b then [] else a :: range (a+1) b;;
val range : int -> int -> int list = <fun>I'm going to rewrite it slightly to make something about the structure of the program clearer (still the same function however):
# let rec range a b = if a > b then [] else let result = range (a+1) b in a :: result;;
val range : int -> int -> int list = <fun>Let's call it:
# List.length (range 1 10);;
- : int = 10 # List.length (range 1 1000000);;
Stack overflow during evaluation (looping recursion?).Hmmm ... at first sight this looks like a problem with recursive programming, and hence with the whole of functional programming! If you write your code recursively instead of iteratively then you necessarily run out of stack space on large inputs, right?
In fact, wrong. Compilers can perform a simple optimisation on certain types of recursive functions to turn them into while loops. These certain types of recursive functions therefore run in constant stack space, and with the equivalent efficiency of imperative while loops. These functions are called tail-recursive functions.
In tail-recursive functions, the recursive call happens last of all. Remember our
loop ()
functions above? They all had the form:let rec loop () = (* do something *) loop ()
Because the recursive call to
loop ()
happens as the very last thing,loop
is tail-recursive and the compiler will turn the whole thing into a while loop.Unfortunately
range
is not tail-recursive, and the longer version above shows why. The recursive call torange
doesn't happen as the very last thing. In fact the last thing to happen is the::
(cons) operation. As a result, the compiler doesn't turn the recursion into a while loop, and the function is not efficient in its use of stack space.Now recall we discussed in the addendum above Eric Cooper's "accumulator" technique, and I said that this technique was widely used in functional programming. We will now see why this is the case. It's because it allows you to write functions such as
range
above in a tail-recursive manner, which means they will be efficient and work properly on large inputs.Let's plan our rewritten
range
function which will use an accumulator argument to store the "result so far":let rec range2 a b accum = (* ... *) let range a b = range2 a b []
(I could and probably should have used a nested function here.)
The
accum
argument is going to accumulate the result. It's the "result so far". We pass in the empty list ("no result so far"). The easy case is whena > b
:let rec range2 a b accum = if a > b then accum else (* ... *)
If
a > b
(ie. if we've reached the end of the recursion), then stop and return the result (accum
).Now the trick is to write the
else
-clause and make sure that the call torange2
is the very last thing that we do, so the function is tail-recursive:# let rec range2 a b accum = if a > b then accum else range2 (a+1) b (a :: accum);;
val range2 : int -> int -> int list -> int list = <fun>There's only one slight problem with this function: it constructs the list backwards! However, this is easy to rectify by redefining range as:
# let range a b = List.rev (range2 a b []);;
val range : int -> int -> int list = <fun>It works this time, although it's a bit slow to run because it really does have to construct a list with a million elements in it:
# List.length (range 1 1000000);;
- : int = 1000000The following implementation is twice as fast as the previous one, because it does not need to reverse a list:
# let rec range2 a b accum = if b < a then accum else range2 a (b-1) (b :: accum);;
val range2 : int -> int -> int list -> int list = <fun> # let range a b = range2 a b [];;
val range : int -> int -> int list = <fun>That was a brief overview of tail recursion, but in real world situations determining if a function is tail recursive can be quite hard. What did we really learn here? One thing is that recursive functions have a dangerous trap for inexperienced programmers. Your function can appear to work for small inputs (during testing), but fail catastrophically in the field when exposed to large inputs. This is one argument against using recursive functions, and for using explicit while loops when possible.