Flots de données
Les streams, ou flots, sont des séquences, potentiellement
infinies, d'éléments de même nature. L'évaluation d'une partie d'un
flot s'effectue à la demande, c'est-à-dire quand cela est nécessaire
pour le calcul en cours : c'est une structure de données
paresseuse.
Le type stream est un type de données abstrait dont on ne
connaît pas l'implantation. On manipule les objets de ce type par
des fonctions de construction et de destruction. Pour le
confort de l'utilisateur, Objective CAML fournit des constructions
syntaxiques simples pour construire des flots et accéder à leurs
éléments.
Warning
Les streams sont une extension du langage et ne font pas
partie du noyau stable d'Objective CAML.
Construction
La syntaxe allégée de construction des flots est inspirée de
celle des liste ou des tableaux. Le flot vide s'écrit :
# [<
>]
;;
- : 'a Stream.t = <abstr>
On peut construire un flot par énumération de ses éléments en
les faisant précéder d'une apostrophe (caractère ')
# [<
'
0
;
'
2
;
'
4
>]
;;
- : int Stream.t = <abstr>
Les expressions qui ne sont pas précédées d'une apostrophe sont
considérées comme des sous-flots :
# [<
'
0
;
[<
'
1
;
'
2
;
'
3
>]
;
'
4
>]
;;
- : int Stream.t = <abstr>
# let
s1
=
[<
'
1
;
'
2
;
'
3
>]
in
[<
s1;
'
4
>]
;;
- : int Stream.t = <abstr>
# let
concat_stream
a
b
=
[<
a
;
b
>]
;;
val concat_stream : 'a Stream.t -> 'a Stream.t -> 'a Stream.t = <fun>
# concat_stream
[<
'
"if"
;
'
"c"
;'
"then"
;'
"1"
>]
[<
'
"else"
;'
"2"
>]
;;
- : string Stream.t = <abstr>
Le module Stream fournit d'autres fonctions de construction.
Par exemple, les fonctions of_channel et of_string
retournent un flot contenant une suite de caractères, à partir d'un
canal d'entrée ou d'une chaîne de caractères.
# Stream.of_channel
;;
- : in_channel -> char Stream.t = <fun>
# Stream.of_string
;;
- : string -> char Stream.t = <fun>
Le calcul retardé des flots permet de manipuler des
structures de données infinies d'une façon similaire au type
'a enum défini à la page ??. On définit
le flot des entiers naturels par son premier élément et une
fonction calculant le flot des éléments suivants.
# let
rec
nat_stream
n
=
[<
'n
;
nat_stream
(n+
1
)
>]
;;
val nat_stream : int -> int Stream.t = <fun>
# let
nat
=
nat_stream
0
;;
val nat : int Stream.t = <abstr>
Destruction et filtrage de flots
La primitive next permet à la fois d'évaluer, de
récupérer et d'enlever le premier élément d'un flot :
# for
i=
0
to
1
0
do
print_int
(Stream.next
nat)
;
print_string
" "
done
;;
0 1 2 3 4 5 6 7 8 9 10 - : unit = ()
# Stream.next
nat
;;
- : int = 11
Lorsque le flot est épuisé, une exception est déclenchée.
# Stream.next
[<
>]
;;
Uncaught exception: Stream.Failure
Pour manipuler les flots, Objective CAML offre une construction de filtrage
dédiée dit filtrage destructif. La valeur filtrée est
calculée et retirée du flot. Il n'y a pas de notion
d'exhaustivité du filtrage des flots et, parce que nous manipulons
une structure paresseuse potentiellement infinie, on peut ne pas
filtrer la totalité du flot. La syntaxe de filtrage est :
Syntaxe
match expr with parser
[< 'p1 ...>] -> expr1
| ...
La fonction next peut s'écrire :
# let
next
s
=
match
s
with
parser
[<
'x
>]
->
x
;;
val next : 'a Stream.t -> 'a = <fun>
# next
nat;;
- : int = 12
Remarquez que l'énumération des entiers reprend là où nous l'avions
laissée.
À la manière de l'abstraction, il existe une forme
syntaxique filtrant un paramètre de type Stream.t.
Syntaxe
parser p -> ...
La fonction next peut alors se réécrire ainsi :
# let
next
=
parser
[<
'x>]
->
x
;;
val next : 'a Stream.t -> 'a = <fun>
# next
nat
;;
- : int = 13
Il est possible de filtrer le flot vide, mais attention : le motif de
flot [<>]
filtre n'importe quel flot. En effet, un flot
s est toujours égal au flot [<
[<>]
;
s
>]
. Il faut
en conséquence inverser l'ordre usuel du filtrage :
# let
rec
it_stream
f
s
=
match
s
with
parser
[<
'x
;
ss
>]
->
f
x
;
it_stream
f
ss
|
[<>]
->
()
;;
val it_stream : ('a -> 'b) -> 'a Stream.t -> unit = <fun>
# let
print_int1
n
=
print_int
n
;
print_string" "
;;
val print_int1 : int -> unit = <fun>
# it_stream
print_int1
[<
'
1
;
'
2
;
'
3
>]
;;
1 2 3 - : unit = ()
En utilisant le fait que le filtrage est destructif, on peut
également écrire :
# let
rec
it_stream
f
s
=
match
s
with
parser
[<
'x
>]
->
f
x
;
it_stream
f
s
|
[<>]
->
()
;;
val it_stream : ('a -> 'b) -> 'a Stream.t -> unit = <fun>
# it_stream
print_int1
[<
'
1
;
'
2
;
'
3
>]
;;
1 2 3 - : unit = ()
Si les flots sont paresseux, ils sont cependant de bonne volonté et
ne refusent jamais de fournir leur premier élément et lorsque
celui-ci est fourni, il est perdu. Ceci a des conséquences sur le
filtrage. La fonction suivante est une tentative (vouée à
l'échec) d'un affichage par couple d'un flot d'entiers, sauf
éventuellement pour le dernier élément.
# let
print_int2
n1
n2
=
print_string
"("
;
print_int
n1
;
print_string
","
;
print_int
n2
;
print_string
")"
;;
val print_int2 : int -> int -> unit = <fun>
# let
rec
print_stream
s
=
match
s
with
parser
[<
'x;
'y
>]
->
print_int2
x
y;
print_stream
s
|
[<
'z
>]
->
print_int1
z;
print_stream
s
|
[<>]
->
print_newline()
;;
val print_stream : int Stream.t -> unit = <fun>
# print_stream
[<
'
1
;
'
2
;
'
3
>]
;;
(1,2)Uncaught exception: Stream.Error("")
Les deux premiers éléments du flot ont bien été affichés,
mais lors de l'évaluation de l'appel récursif
(print_stream
[<
3
>]
) le premier motif a trouvé une
valeur pour x, qui a donc été consommée, mais il ne
restait plus rien pour y. C'est ce qui a provoqué
l'erreur. En fait, le second filtre est inutile puisque, si le flot
n'est pas vide, le premier motif peut toujours commencer
l'évaluation.
Pour obtenir le résultat attendu, il faut séquentialiser le
filtrage :
# let
rec
print_stream
s
=
match
s
with
parser
[<
'x
>]
->
(match
s
with
parser
[<
'y
>]
->
print_int2
x
y;
print_stream
s
|
[<>]
->
print_int1
x;
print_stream
s)
|
[<>]
->
print_newline()
;;
val print_stream : int Stream.t -> unit = <fun>
# print_stream
[<
'
1
;
'
2
;
'
3
>]
;;
(1,2)3
- : unit = ()
Si le filtrage échoue sur le premier élément d'un filtre alors,
on retrouve le comportement connu du filtrage :
# let
rec
print_stream
s
=
match
s
with
parser
[<
'
1
;
'y
>]
->
print_int2
1
y;
print_stream
s
|
[<
'z
>]
->
print_int1
z;
print_stream
s
|
[<>]
->
print_newline()
;;
val print_stream : int Stream.t -> unit = <fun>
# print_stream
[<
'
1
;
'
2
;
'
3
>]
;;
(1,2)3
- : unit = ()
Aux limites du filtrage
Par son caractère destructif, le filtrage des flots diffère
du filtrage des types somme. Nous allons voir comment il peut en
différer radicalement.
On peut écrire, de façon naturelle, une fonction qui calcule la
somme des éléments d'un flot :
# let
rec
somme
s
=
match
s
with
parser
[<
'n;
ss
>]
->
n+
(somme
ss)
|
[<>]
->
0
;;
val somme : int Stream.t -> int = <fun>
# somme
[<
'
1
;
'
2
;
'
3
;
'
4
>]
;;
- : int = 10
Mais on peut également consommer le flot << de l'intérieur >> en
nommant le résultat obtenu :
# let
rec
somme
s
=
match
s
with
parser
[<
'n;
r
=
somme
>]
->
n+
r
|
[<>]
->
0
;;
val somme : int Stream.t -> int = <fun>
# somme
[<
'
1
;
'
2
;
'
3
;
'
4
>]
;;
- : int = 10
Nous étudierons d'autres utilisations importantes des flots dans le
chapitre 11 consacré aux analyses lexicale et
syntaxique. En particulier, nous verrons comment la consommation d'un
flot de l'intérieur peut être mise à profit.