Stream Expressions
NOTE: the stream expressions are based on camlp4, whose usage is discouraged in modern OCaml as it's incompatible with ppx (syntactic preprocessors).
Stream Expressions
Streams are very powerful and concise once the necessary tools (stream builders, combinators, and other utilities) are in place. However, the code behind these tools can be a chore to write, at times resembling state machines more than elegant functional expressions. The Camlp4 preprocessor adds syntax support for stream and stream parser expressions, raising the level of abstraction a bit higher and simplifying many common stream-oriented tasks.
Interactive Use
To enable stream expression support in the interactive toplevel, you can type the following:
# #load "dynlink.cma";;
# #load "camlp4o.cma";;
Line 1:
Error: Reference to undefined global `Stdlib__stream'
Or, if you are using findlib:
#use "topfind";;
#camlp4o;;
Stream Literals
Stream expressions are enclosed by [<
and >]
brackets, and using
them is a lot like using lists. The simplest stream possible is the
empty stream:
# [< >];;
Error: Syntax error
Literal values in stream expressions are prefixied by single-quotes:
# let more_numbers = [< '1; '2; '(1 + 2) >]
(* Equivalent to Stream.of_list [1; 2; 3] *);;
Error: Syntax error
This is to distinguish them from streams, which are automatically concatenated:
# [< '1; '2; more_numbers; '99 >];;
Error: Syntax error
In the above example, the stream will produce the integers 1 and 2,
followed by all of the values generated by the more_numbers
stream,
and once more_numbers
as been exhausted, it will produce the integerĀ 99.
Recursive Definition
Streams can be defined with recursive functions, providing a straightforward and familiar mechanism to produce infinite sequences. The following defines an never-ending stream of 1s:
# let ones =
let rec aux () =
[< '1; aux () >] in
aux ();;
Error: Syntax error
Note the auxiliary function, aux
, which is called recursively to form
a loop. This is necessarily different from infinite lists, which can be
defined with "let rec" without a helper function:
# let rec ones = 1 :: ones;;
val ones : int list = [1; <cycle>]
The stream expression syntax is not able to interpret recursive values like this, and attempts to do this will result in an error:
# let rec ones = [< '1; ones >];;
Error: Syntax error
This is only a minor inconvenience, since most streams will be built
from one or more parameters. For example, here is the const_stream
from the Streams chapter, redefined using
stream expression syntax:
# let rec const_stream k = [< 'k; const_stream k >];;
Error: Syntax error
Similarly, this simple one-liner is all it takes to produce a counter:
# let rec count_stream i = [< 'i; count_stream (i + 1) >];;
Error: Syntax error
Below is a slightly more complicated example, using two stream expressions to define a Fibonacci sequence:
# let fib =
let rec aux a b =
[< '(a + b); aux b (a + b) >] in
[< '0; '1; aux 0 1 >];;
Error: Syntax error
# Stream.npeek 10 fib;;
Error: Unbound value fib
Example: Directory walker
As a more practical example, we can define a recursive directory walker that avoids loading the entire directory tree into memory:
# let rec walk dir =
let items =
try
Array.map
(fun fn ->
let path = Filename.concat dir fn in
try
if Sys.is_directory path then `Dir path
else `File path
with e -> `Error (path, e))
(Sys.readdir dir)
with e -> [| `Error (dir, e) |] in
Array.fold_right
(fun item rest ->
match item with
| `Dir path -> [< 'item; walk path; rest >]
| _ -> [< 'item; rest >])
items
[< >];;
Error: Syntax error: operator expected.
This function works by first assembling an array of paths for the
specified base directory. Each path is wrapped in a variant type that
keeps track of which path is a file and which is a directory. The array
is then folded into a stream, expanding each subdirectory by recursively
calling walk
again. Errors are included in the variant so that
exceptions can be handled or ignored as needed.
The expanding of subdirectories
[< 'item; walk path; rest >]
illustrates a convenient feature of stream expressions: any number of
sub-streams can appear in any order, and they will be lazily evaluated
as needed. walk path
and rest
both evaluate to streams that are
concatenated with item
at the front. The results are flattened into a
single stream, just as if we had used something like stream_concat
,
defined in the Streams chapter.
With little effort, we can now print the names of all the directories
and files underneath
site/caml-light
:
# let () =
Stream.iter
(function
| `Dir path -> Printf.printf "dir: %s%!\n" path
| `File path -> Printf.printf "file: %s%!\n" path
| `Error (path, e) ->
Printf.printf "error: %s (%s)%!\n" path
(Printexc.to_string e))
(walk "site/caml-light");;
Error: Unbound value walk