Showing posts with label non-strict. Show all posts
Showing posts with label non-strict. Show all posts

Monday, January 18, 2010

Introducing Streams

Streams are a special type of Iterable/Traversable whose elements are not evaluated until they are requested. Streams are normally constructed as functions.

A Scala List basically consists of the head element and the rest of the list. A Scala Stream is the head of the stream and a function that can construct the rest of the Stream. It is a bit more than this because each element is evaluated only once and stored in memory for future evaluations. That should be more clear after some examples.

As usual I created these examples with the Scala 2.8 REPL but I think most if not all should work in 2.7.

  1. scala> import Stream.cons          
  2. import Stream.cons
  3. /*
  4. Because streams are very functional in nature it is recommended that methods from the Stream object are used for creation
  5. This is a real boring example of creating a Stream.  Anything this simple should be a list.
  6. The important part is that cons take the value at the point and a function to return the rest of the
  7. stream NOT another stream.  
  8. */
  9. scala> val stream1 = cons(0,cons(1,Stream.empty))
  10. stream1: Stream.Cons[Int] = Stream(0, ?)
  11. scala> stream1 foreach {print _}                 
  12. 01
  13. /*
  14. This illustrates the similarity in design between Stream and list, again the difference is the entire list is created in a stream the second argument of cons is not evaluated until it is requested
  15. */
  16. scala> new ::(0, new ::(1,List.empty))
  17. res35: scala.collection.immutable.::[Int] = List(0, 1)
  18. /*
  19. To drive home the point of the similarities.  Here is an alternative declaration of a
  20. stream most similar to declaring a list
  21. */
  22. scala> val stream2 = 0 #:: 1 #:: Stream.empty    
  23. stream2: scala.collection.immutable.Stream[Int] = Stream(0, ?)
  24. scala> stream2 foreach {print _}
  25. 01
  26. scala> 0 :: 1 :: Nil
  27. res36: List[Int] = List(0, 1)
  28. /*
  29. A little more interesting now.  The accessing the second element will run the function.  Notice it is not evaluated until request
  30. */
  31. scala> val stream3 = cons (0, {    
  32.      | println("getting second element")
  33.      | cons(1,Stream.empty)
  34.      | })
  35. stream3: Stream.Cons[Int] = Stream(0, ?)
  36. scala> stream3(0)
  37. res56: Int = 0
  38. // function is evaluated
  39. scala> stream3(1)
  40. getting second element
  41. res57: Int = 1
  42. /* 
  43. Function is only evaluated once.  
  44. Important! This means that all elements in a Stream are loaded into a memory so
  45. it can cause a OutOfMemoryError if the stream is large
  46. */
  47. scala> stream3(1)
  48. res58: Int = 1
  49. scala> stream3(1)
  50. res59: Int = 1
  51. /*
  52. This creates an infinate stream then forces resolution of all elements
  53. */
  54. scala> Stream.from(100).force            
  55. java.lang.OutOfMemoryError: Java heap space
  56. // Alternative demonstration of laziness
  57. scala> val stream4 = 0 #:: {println("hi"); 1} #:: Stream.empty
  58. stream4: scala.collection.immutable.Stream[Int] = Stream(0, ?)
  59. scala> stream4(1)
  60. hi
  61. res2: Int = 1

A very common way to construct a Stream is to define a recursive method. Each recursive call constructs a new element in the stream. The method may or may not have a guard condition that terminates the stream.
  1. // construct a stream of random elements
  2. scala> def make : Stream[Int] = Stream.cons(util.Random.nextInt(10), make)
  3. make: Stream[Int]
  4. scala> val infinate = make                                                
  5. infinate: Stream[Int] = Stream(3, ?)
  6. scala> infinate(5)                                  
  7. res10: Int = 6
  8. scala> infinate(0)
  9. res11: Int = 3
  10. // Once evaluated each element does not change
  11. scala> infinate(5)
  12. res13: Int = 6
  13. // this function makes a stream that does terminate
  14. scala> def make(i:Int) : Stream[String] = {                  
  15.      | if(i==0) Stream.empty                                 
  16.      | else Stream.cons(i + 5 toString, make(i-1))           
  17.      | }
  18. make: (i: Int)Stream[String]
  19. scala> val finite = make(5)                       
  20. finite: Stream[String] = Stream(10, ?)
  21. scala> finite foreach print _                     
  22. 109876
  23. // One last demonstration of making a stream object
  24. scala> Stream.cons("10", make(2))
  25. res18: Stream.Cons[String] = Stream(10, ?)
  26. /*
  27. This method is dangerous as it forces the entire stream to be evaluated
  28. */
  29. scala> res18.size
  30. res19: Int = 3


This is only an introduction. I hope to add a few more topics that focus on Streams because they can be very powerful but are also more challenging to recognize where they should be used instead of a standard collection.

Tuesday, November 3, 2009

Non-strict (lazy) Collections

This topic discusses non-strict collections. There is a fair bit of confusion with regards to non-strict collections: what to call them and what are they. First lets clear up some definitions.

Pre Scala 2.8 collections have a projection method. This returns a non-strict collections. So often non-strict collections are called projections in Pre Scala 2.8
Scala 2.8+ the method was changed to view, so in Scala 2.8+ view sometimes refers to non-strict collections (and sometime to a implicit conversion of a class)
Another name for non-strict collections I have seen is "lazy collections."

All those labels are for the same thing "non-strict collections" which is the functional programming term and which I will use for the rest of this topic.

As an excellent addition to this topic please take a look at Strict Ranges? by Daniel Sobral.

One way to think of non-strict collections are pull collections. A programmer can essentially form a sequence of functions and the evaluation is only performed on request.

Note: I am intentionally adding side effects to the processes in order to demonstrate where processing takes place. In practice the collectionsshould be immutable (ideally) and the processing the collections really should be side-effect free. Otherwise almost guaranteed you will find yourself with a bug that is almost impossible to find.

Example of processing a strict collection:
  1. scala> var x=0
  2. x: Int = 0
  3. scala> def inc = {
  4.      | x += 1
  5.      | x
  6.      | }
  7. inc: Int
  8. scala> var list =  List(inc _, inc _, inc _)
  9. list: List[() => Int] = List(<function0><function0><function0>)
  10. scala> list.map (_()).head
  11. res0: Int = 1
  12. scala> list.map (_()).head
  13. res1: Int = 4
  14. scala> list.map (_()).head
  15. res2: Int = 7

Notice how each time the expression is called x is incremented 3 times. Once for each element in the list. This demonstrates that map is being called for every element of the list even though only head is being calculated. This is strict behaviour.

Example of processing a non-strict collection with Scala 2.7.5
For Scala 2.8 change project => view.
  1. scala> var x=0
  2. x: Int = 0
  3. scala> def inc = {
  4.      | x += 1
  5.      | x
  6.      | }
  7. inc: Int
  8. scala> var list =  List(inc _, inc _, inc _)
  9. list: List[() => Int] = List(<function0><function0><function0>)
  10. scala> list.projection.map (_()).head
  11. res0: Int = 1
  12. scala> list.projection.map (_()).head
  13. res1: Int = 2
  14. scala> list.projection.map (_()).head
  15. res2: Int = 3
  16. scala> list.projection.map (_()).head
  17. res3: Int = 4

Here you can see that only one element in the list is being calculated for the head request. That is the idea behind non-strict collections and can be useful when dealing with large collections and very expensive operations. This also demonstrates why side-effects are so crazy dangerous!.

More examples (Scala 2.8):
  1. scala> var x=0
  2. x: Int = 0
  3. scala> def inc = { x +=1; x }
  4. inc: Int
  5. // strict processing of a range and obtain the 6th element
  6. // this will run inc for every element in the range
  7. scala> (1 to 10).map( _ + inc).apply(5)
  8. res2: Int = 12
  9. scala> x
  10. res3: Int = 10
  11. // reset for comparison
  12. scala> x = 0
  13. x: Int = 0
  14. // now non-strict processing but the same process
  15. // you get a different answer because only one
  16. // element is calculated
  17. scala> (1 to 10).view.map( _ + inc).apply(5)
  18. res6: Int = 7
  19. // verify that x was incremented only once
  20. scala> x
  21. res7: Int = 1
  22. // reset for comparison
  23. scala> x = 0
  24. x: Int = 0
  25. // force forces strict processing
  26. // now we have the same answer as if we did not use view
  27. scala> (1 to 10).view.map( _ + inc).force.apply(5)
  28. res9: Int = 12
  29. scala> x
  30. res10: Int = 10
  31. // reset for comparison
  32. scala> x = 0
  33. x: Int = 0
  34. // first 5 elements are computed only
  35. scala> (1 to 10).view.map( _ + inc).take(5).mkString(",")
  36. res9: String = 2,4,6,8,10
  37. scala> x
  38. res10: Int = 5
  39. // reset for comparison
  40. scala> x = 0
  41. x: Int = 0
  42. // only first two elements are computed
  43. scala> (1 to 10).view.map( _ + inc).takeWhile( _ < 5).mkString(",")
  44. res11: String = 5,7
  45. scala> x
  46. res12: Int = 5
  47. // reset for comparison
  48. scala> x = 0
  49. x: Int = 0
  50. // inc is called 2 for each element but only the last 5 elements are computed so
  51. // x only == 10 not 20
  52. scala> (1 to 10).view.map( _ + inc).map( i => inc ).drop(5).mkString(",")
  53. res16: String = 2,4,6,8,10
  54. scala> x
  55. res17: Int = 10
  56. scala> x = 0                                             
  57. x: Int = 0
  58. // define this for-comprehension in a method so that
  59. // the repl doesn't call toString on the result value and
  60. // as a result force the full list to be processed
  61. scala> def add = for( i <- (1 to 10).view ) yield i + inc
  62. add: scala.collection.IndexedSeqView[Int,IndexedSeq[_]]
  63. scala> add.head                                          
  64. res5: Int = 2
  65. // for-comprehensions will also be non-strict if the generator is non-strict
  66. scala> x
  67. res6: Int = 1