Streams provide a way of working with ordered sets of data. In lecture Ramin gave the example that a stream is like the signal going through your stereo system. Your cd player reads one number off of the cd disk and sends it to the amp. The amp then changes the number into a sound and sends it to your speakers. The key feature of a stream is that functions (or stereo components) work on the stream a little bit at a time; you don't need to know all the values in the stream to start playing with the first one. Also in lecture, Ramin gave a treatment of streams using lazily evaluated lists. He did this in the context of mini-ml which was hacked specifically to accommodate streams. These notes concern working with streams in straight SML. First off let's define a stream type and try to create the stream of integers 1 though 10. Like lists we can consider streams to be made up of head and tail. What type should we use for a stream? Let's try: datatype 'a stream = STREAM of 'a * 'a stream The type looks ok and it typechecks. However we run into a problem trying to build an instance of the stream. The first item in the stream needs to be a one: STREAM(1, ?) The ? mark has to be something of type int stream, which makes our expression STREAM(1, STREAM(2, ?)) and so on: SREAM(1, STREAM(2, STREAM(3, ...(STREAM(10, ?) ))) We run into two problems here. First we're done with the stream, but still need something to fit into the ?. From the types we know ?: int stream We don't want any more numbers here so lets extend the definition of 'a stream to dataype 'a stream = STREAM of 'a * 'a stream | NIL The above looks a lot like the definition of a list and this suggests the second problem with these streams: they do not delay evaluation. That is, we must know the values 1..10 in order to write STREAM(1, STREAM(2, STREAM(3, ...(STREAM(10, NIL) ))) We can even write a function to build this guy for us: fun builder(n :int, m:int): int stream = if n <= m then STREAM(n, build(n+1,m) ) else NIL We still have problem though, because the entire point of a stream is to allow computation on the beginning of the stream before the rest of the stream is known. We need a way of delaying evaluation of the tail of the stream. In lecture Ramin used a thunk, which forcced the returned another the rest of the stream, to delay evaluation. In normal SML we don't have built in thunks so we need to build this delay ourselves. Let's alter the definition of streams such that you have to evaluate a function (given unit) to the tail: datatype 'a stream = STREAM of 'a * (unit -> 'a stream) | NIL This definition is a little weird, but makes some sense. All we've done is two force the program to explicitly ask for the tail to be evaluated. Let's try building our stream from 1 to 10. STREAM(1, ?) is a start, ?: unit -> 'a stream so let's try STREAM(1, fn()=>STREAM(2, ?)) this looks a lot like what we had before and we can adapt the builder function to it fun builder(n,m) = if n <= m then STREAM(n, fn()=> builder(n+1,m)) else NIL We'd like to do something useful to streams, but first we need a way to examine the values that a stream creates. Let's create a function toList: int -> 'a stream -> 'a list which puts the beginning of a stream in a list. We use the integer to decide how many stream elements to add to the list. This is useful because we don't want to force evaluation of the entire stream. fun toList (n: int) (s:'a stream): 'a list = if n>0 then case s of STREAM(x,xs) => x::toList (n-1) (xs()) | NIL => nil else nil Note that this looks like a lot like a recursive function on lists, except that we apply the stream's tail, xs, to unit before the recursive call. for example : - val myStream = builder 1; val myStream = STREAM (1,fn) : int stream - toList 3 myStream; val it = [1,2,3] : int list - toList 12 myStream; val it = [1,2,3,4,5,6,7,8,9,10] : int list We can do all the same things we like to do lists to streams. For example we could write fold, map, filter, zip. Let's focus on filter and map. Filter's job is to select elements from a stream which satisfy a predicate. We can write this as fun filter (f: 'a -> bool) (s: 'a stream): 'a stream = case s of STREAM (x, xs) => if (f x) then STREAM(x, fn() => (filter f ( xs() )) ) else filter f ( xs() ) | NIL => NIL We could use this filter to remove odd numbers from a stream. For example: - toList 12 (filter (fn x=> x mod 2 = 0) myStream); val it = [2,4,6,8,10] : int list A slighlt more interesting example: val cuteNumber = (String.isPrefix "312") o Int.toString; fun isPrime (x:int): bool = let fun f(n:int): bool = if n*n > x then true else (x mod n <> 0) andalso f(n+1) in f(2) end - toList 15 ((filter isPrime) ((filter cuteNumber) (builder (1,312000)) ) ); val it = [3121,31219,31223,31231,31237,31247,31249,31253,31259,31267,31271,31277] : int list Note a few interesting things here. We filter by the cheap cuteNumber function before filtering by isPrime. This if the difference between execution taking one second and four seconds. If we try to do this calculation using lists: ((List.filter cuteNumber) ((List.filter isPrime) (List.tabulate (312000,fn x=>x)))) We need to allocate a large quantity of memory and execution time is about two seconds. Recap: Streams are like lists, except you don't calculate tail until you really need too. Tomorrow in lecture, Ramin will introduce infinite streams.