Don't be lazy about lazy sequences. 33

Posted by Uncle Bob Thu, 06 Aug 2009 16:54:41 GMT

Coding with infinite sequences is very powerful, but also very bizarre. If you aren’t careful you can get algorithms that work fine, but are O(N^2) instead of O(N).

Consider the (partial-sums) function that I used in my previous blog on generating pi.

(defn partial-sums [s]
  (lazy-seq
    (cons (first s) (add-streams (next s) (partial-sums s)))))

This function takes in a list of (a b c…) and returns a list of partial sums (a a+b a+b+c…). Trying to figure out how this function works is a bit mind-bending; at least for me; but here’s how I visualize it.

It adds (next s) to (partial-sums s).
                   (next s)
                      |
                      V
              s: (a   b    c     d ...)
(partial-sum s):     (a   a+b  a+b+c ...)

Notice how the vertical columns are the sums that we want. Notice also that each result is the sum of the previous result and the next element of s. So verbally and visually this all makes sense. But algorithmically it can still be challenging to understand. I mean, how do those values get calculated really?

Oh… And I should note… The function above doesn’t work this way at all. It’s actually horrifically inefficient. But I’ll get to that, and to the solution. First things first.

What allows this function to run is lazy-evaluation. All that really means is that an element of a list does not really exist until you ask for it. At that point the program calculates what the element of the list should be and then returns it to you as if it was there all along.

In Clojure, when you build a sequence inside the (lazy-seq) form, it simply defers the execution of the procedures that generate the values of the sequence until those values are requested. Look again at the function above. When you call it, it simply returns immediately without doing anything. The (lazy-seq) form simply short-circuits the execution of the function, deferring it for later.

So when (partial-sums) returns it gives you a sequence, but there’s nothing in it! However, as soon as you ask for the first element, the body of the (partial-sums) function will execute just enough to give you the value of the first element. No other elements are calculated. When you ask for the next value, (partial-sums) will execute again, just enough to give you that value. And so on.

Knowing that, let’s trace the execution.

  • We call (partial-sums s) which returns a list (let’s call it ps)
  • To get the first element of ps, (partial-sums) is lazily-invoked and simply returns (first s)
  • For the second element it returns the first element of the sequence produced by (add-streams (next s) (partial-sums s))
    which will be: (+ (first (next s)) (first s))
  • For the third element it returns the second element of the added streams, but the element is composed of yet another stream addition invoked by the recursive (partial-sums) So it looks like this:
    (+ (first (next (next s)) (+ (first (next s)) (first s))

“Uh… Wait.” I hear you say. “You mean that in order to calculate the third element, you have to add three elements? Didn’t we say that we could add the next element of s to the previous element of the partial sums? Why do we have to do three additions? Why does it seem to be doing this:”

(a b c)
  (a b c)
    (a b c)
Notice that the vertical columns are the sums we are looking for; but calculating them requires a lot more work than just adding the next value to the previous sum!.

“But wait,” you say again. “I know something about Clojure. Clojure memoizes the list values. Once you ask for a list element the first time, it never has to recalculate it again! So when we get the third element of the result it shouldn’t have to recalculate the second.”

Partly right. Once you lazily evaluate a list element, that element never has to be evaluated again. It’s in the list, and will simply be returned from the list. But let me ask you this: In the function above, where is the list that being asked again? Look closely. There is no partial-sums result that is being asked for a value more than once. Indeed, every time we need a value we create a new (partial-sums) list. So we aren’t taking advantage of the previous values because we’re always creating a new list.

If you’d like to prove this to yourself, you can run this fun little program.
(defn p [a b] (println a '+ b) (+ a b))

(defn add_streams [a b] (map p a b))

(defn partial-sums [s]
    (lazy-seq
      (cons (first s)
        (add_streams (next s) (partial-sums s)))))

(def w (iterate inc 1))
(println (take 5 (partial-sums w)))
Notice how I print every addition in the (p) function. If you run this you’ll get the following result:
(2 + 1
1 2 + 1
3 + 3
3 2 + 1
3 + 3
4 + 6
6 2 + 1
3 + 3
4 + 6
5 + 10
10 15)

The jumble of numbers is evidence that the lazy evaluation is working. As the println marches through the 5 partial sums, it causes each to be evaluated, causing each sum to be printed. Notice how many sums are taken! Ten additions for 5 partial sums. It’ll be 15 for 6, and 21 for 7. Indeed, the number of additions required to calculate ps[n] is equal to ps[n-1]. A little thought will convince you that this is O(n^2).

To reduce this back to O(n), we need to stop creating a new list of partial-sums at each recursion, and use a single copy of the partial-sums result. We can do this by saving the partial sums result in a var, and then using that in the add-streams call.

(defn partial-sums [s]
    (def result (lazy-seq
      (cons (first s)
        (add_streams (next s) result)))) result)

This may bend your brain just a little bit more. We are using the result before we are done calculating it! But that’s ok, since we never use more than has already been calculated…

Running the program with this change yields the following output.

(2 + 1
1 3 + 3
3 4 + 6
6 5 + 10
10 15)

That’s the O(n) behavior we wanted! Each sum is calculated by adding the next element to the previous sum! Hooray!

There’s a lesson here. You don’t get the benefit of lazy sequences unless you use the lazy sequences. Creating a brand new one each time may generate the right answer, but it’s lazy, and can burn a lot of execution cycles.

One problem I have with this solution is the creation of the global var ‘result’. I’m sure there’s a way to create a local result; but I haven’t found it yet. (I tried using various let forms, but I haven’t stumbled upon one that works yet.)

Comments

Leave a response

  1. Avatar
    Rich Hickey 44 minutes later:

    There’s a function in clojure-contrib that does what you want, and demonstrates how to do it:

    (use ‘clojure.contrib.seq-utils)

    (reductions + (range 10))

    => (0 1 3 6 10 15 21 28 36 45)

  2. Avatar
    Gregor Gramlich about 2 hours later:
    This mind-bending trace of execution

    (+ (first (next (next s)) (+ (first (next s)) (first s))

    suddenly reminded me of a book that I read five years ago. Donald Knuth’s “Surreal numbers”, but I just vaguely remember it.

    You want to emphasize quadratic lower bounds, but write O(n²). It should be Ω(n²) or Θ(n²).

  3. Avatar
    Kooba Handbags 8 months later:

    Living without an aim is like sailing without a compass. with a new http://www.handbags4buy.com/ idea is a crank until the idea succeeds.

  4. Avatar
    moncler clearance 8 months later:

    Very quietly I take my leave.To seek a dream in http://www.edhardy-buy.com/ starlight.

  5. Avatar
    DVD to HTC 9 months later:

    fgd thanks,the post is really good PDF to BMP Converter will give you the satisfied

    output quality and conversion speed. Whether you are professional or novice, you can apply it without any errors. You can even set

    the personalized output effect

  6. Avatar
    MKV to iPad 10 months later:

    that real significant changes are often the ones that go unnoticed, because they Make Things Work As They Are Supposed To.

  7. Avatar
    Oliva Terrace about 1 year later:

    This is a good,common sense article.Very helpful to one who is just finding bamboo kitchen the resouces about this part.It will certainly help educate me.

  8. Avatar
    bag manufacturer about 1 year later:

    ul, but also very bizarre. If you aren’t careful you can get algorithms that work

  9. Avatar
    moncler about 1 year later:

    I know something about Clojure. Clojure memoizes the list values. Once you ask for a list element the first time, it never has to recalculate it again! So when we get the third element of the result it shouldn’t have to recalculate the second

  10. Avatar
    canada goose about 1 year later:

    we offer you canada goose to enjoy the winter!! canada goosewarm canada goose parka protection canada goose expeditionfashionable canada goose jakkerlet you have Happy winter canada goose jakkeGood quality

    Canada goose jakkesnug Goose jakkeYou’re Worth It Canada goose jakkerremain stylish Canada goose parkais a trustworthy brand Canada goose tilbudis the best one

    canada goose jacka vogue goose jacka pretty canada goose jackordifferent designs outlet canada goose amazing canada goose Expedition smooth welcome to buy!thank you!

  11. Avatar
    Pandora about 1 year later:

    but that all attempts to improve quality would be short lived and followed by a larger drive to decrease quality even further.

  12. Avatar
    Moncler4u about 1 year later:

    Moncler jackets are not ordinary.

  13. Avatar
    moncler about 1 year later:

    Very few has the knack of writing such beautiful post like you do.. !!moncler mens I have been following this blog and i feel different energy while reading your post. Keep it dear buddy.. !!moncler women

  14. Avatar
    gearbox about 1 year later:

    We are engaged in gearbox,radial piston motor,axial piston motor,piston motor,slewing transmission,danfoss motor,hydraulic orbital motor,hydraulic steering,hydraulic steering unit,hydraulic winch. All products are strictly tested before delivery by testing bench and comprehensive testing facilities to ensure the quality.

  15. Avatar
    Criminal Check about 1 year later:

    This is a great source of stuff, worth reading. Thanks for this awesome information.

  16. Avatar
    Solid State Relay about 1 year later:

    This article is very usefull for me! I can see that you are putting a lots of efforts into your blog. I will keep watching in your blog, thanks.

  17. Avatar
    Criminal Records about 1 year later:

    This may bend your brain just a little bit more. We are using the result before we are done calculating it! But that’s ok, since we never use more than has already been calculated…

  18. Avatar
    Sunglass about 1 year later:

    Sunglass Sunglass Sunglass

  19. Avatar
    derica about 1 year later:

    We got a fine book on that matter from our local library and most books were not as descriptive as your information

  20. Avatar
    tiffany and co outlet about 1 year later:

    A bad beginning makes a bad ending. A bird in the hand is worth than two in the bush. A year’s plan starts with

    spring. Bad news has wings. Better to ask the way than go astray. By reading we enrich the mind, by conversation we

    polish it. Custom makes all things easy. He is not fit to command others that cannot command himself. He laughs best

    who laughs last. tiffany and co outlet

  21. Avatar
    tiffany and co outlet about 1 year later:

    A bad beginning makes a bad ending. A bird in the hand is worth than two in the bush. A year’s plan starts with

    spring. Bad news has wings. Better to ask the way than go astray. By reading we enrich the mind, by conversation we

    polish it. Custom makes all things easy. He is not fit to command others that cannot command himself. He laughs best

    who laughs last. tiffany and co outlet

  22. Avatar
    okey oyunu oyna about 1 year later:

    good information. Thanks.

    internette görüntülü olarak okey oyunu oyna, gerçek kisilerle tanis, turnuva heyecanini yasa.

  23. Avatar
    cheap brand watches about 1 year later:

    It is really a nice post, it is always great reading such posts, this post is good in regards of both knowledge as well as information. Very fascinating read, thanks for sharing this post here.

  24. Avatar
    Jewellery about 1 year later:

    Online UK costume and fashion jewellery shop with, i

  25. Avatar
    beats by dr dre headphones about 1 year later:

    Some beats by dr dre solo purple training routines that will improve Your Voice instantly when you exercise Them!These training routines are extremely effective at erasing bad habits, and replacing them using a “successful”, fabulous sounding voice. The headphone’s exterior is produced from an ultra glossy complete that’s particular to garner some attention in beats by dr dre pro black.

  26. Avatar
    ENWOO over 2 years later:

    Enwoo enjoys a leading position among the industry with its advanced technology and management concept after years of hardworking. Electron beam welding machine China enwoo Electron beam, Beam Welding Machine,Beam Welding Machine Beam Welding Machine,beam welding Multiplex electron-beam welding , High-efficient electron, Electron beam welding machine,Enwoo high-pressure tank

  27. Avatar
    beats by dre store over 2 years later:

    Very fascinating read, thanks for sharing this post here.cheap beats by dre beats by dre store

  28. Avatar
    Tips For Bowling over 2 years later:

    The reason sport is attractive to many of the general public is that it’s filled with reversals. What you think may happen doesn’t happen. A champion is beaten, an unknown becomes a champion.

  29. Avatar
    ysbearing/yisong@1stbearing.com over 2 years later:

    Slewing bearing called slewing ring bearings, is a comprehensive load to bear a large bearing, can bear large axial, radial load and overturning moment. http://www.1stbearing.com

  30. Avatar
    canada goose jakke over 2 years later:

    Om canada goose jakke spørgsmål vedrørende fødevaresikkerhed, canada goose Gruppen af otte forventes canada goose outlet at lancere “Aquila fødevarer belstaff outlet security initiative”, vil være os $ 10-15 milliarderi belstaff støtte begået for landbrugs udvikling i fattigere Canada Goose Ungdom Freestyle lande.Med hensyn til finansforordningen forventes gruppen af otte ledere at være “Lecce rammen” Hot Salg på grundlag af vedtagelsen canada goose jakke af en politikfil, cross-domæne Canada Goose Banff Parka for at styrke tilsynet med banker, institutioner og virksomhedsledelse, og I am legend og har Hot Salg for nylig udgivet selvmord.I dag, efteråret Canada Goose Handsker på ny hylden, ikke kun mode, men vil holde dig tilsluttet på legender af læder.

  31. Avatar
    christian louboutin over 2 years later:

    The professional design make you foot more comfortable. Even more tantalizing,this pattern make your legs look as long as you can,it will make you looked more attractive.Moveover,it has reasonable price.If you are a popular woman,do not miss it.

    Technical details of Christian Louboutin Velours Scrunch Suede Boots Coffee:

    Color: Coffee
    Material: Suede
    4(100mm) heel
    Signature red sole x

    Fashion, delicate, luxurious Christian louboutins shoes on sale, one of its series is Christian Louboutin Tall Boots, is urbanism collocation. This Christian louboutins shoes design makes people new and refreshing. Red soles shoes is personality, your charm will be wonderful performance.

  32. Avatar
    gianmarco lorenzi over 2 years later:

    water into water,

  33. Avatar
    Discount Louboutin Shoes over 2 years later:

    Every women always has Christian Louboutins Wedding Shoes turn of fame but it also has its own goodbyes.

Comments