Trouble understanding / visualising SICP streams Hamming numbers program
Asked Answered
I

2

3

I'm basically stuck at excercise 3.56 in SICP. The problem goes like this:

Exercise 3.56. A famous problem, first raised by R. Hamming, is to enumerate, in ascending order with no repetitions, all positive integers with no prime factors other than 2, 3, or 5. One obvious way to do this is to simply test each integer in turn to see whether it has any factors other than 2, 3, and 5. But this is very inefficient, since, as the integers get larger, fewer and fewer of them fit the requirement. As an alternative, let us call the required stream of numbers S and notice the following facts about it.

  • S begins with 1.
    • The elements of (scale-stream S 2) are also elements of S.
    • The same is true for (scale-stream S 3) and (scale-stream 5 S).
    • These are all the elements of S.

Now all we have to do is combine elements from these sources. For this we define a procedure merge that combines two ordered streams into one ordered result stream, eliminating repetitions:

(define (merge s1 s2)
   (cond ((stream-null? s1) s2)
         ((stream-null? s2) s1)
         (else
          (let ((s1car (stream-car s1))
                (s2car (stream-car s2)))
            (cond ((< s1car s2car)
                   (cons-stream s1car (merge (stream-cdr s1) s2)))
                  ((> s1car s2car)
                   (cons-stream s2car (merge s1 (stream-cdr s2))))
                  (else
                   (cons-stream s1car
                                (merge (stream-cdr s1)
                                       (stream-cdr s2)))))))))

Then the required stream may be constructed with merge, as follows:

(define S (cons-stream 1 (merge <??> <??>)))

Fill in the missing expressions in the places marked above.

Before this particular problem, I've been able to visualize and understand these implicit stream definitions using a signal processing block diagram with the original stream being fed back to the procedure.

But I've basically hit a wall with this particular problem, I've looked up the solution, but I'm finding it impossible to visualize how the solution would work in my head/paper.

Is there a trick for understanding and coming up with solutions for these sort of problems?

This is the solution that works:

(define S 
  (cons-stream 1 (merge (scale-stream S 2)
                        (merge (scale-stream S 3)
                               (scale-stream S 5)))))

Thanks in advance.

Impart answered 11/4, 2019 at 15:21 Comment(2)
Now that you've seen the answer, have you tried working through how Scheme expands these expressions before evaluating them? I think this may help you understand what is going on here (use the equivalent definition of cons-stream given by the book that uses delay as you manually expand it.) I'd recommend working through the expansion of the stream at least until you reach 6 (the lowest number in the stream which is a multiple of two of the different factors).Spinal
try coding it with explicit objects instead, expressed as closures with mutable state, which explicitly pull their input from suppliers to produce their output (as one possible model of generators). you will discover lots of hidden stuff here, possibilities and choices to make (cf. Python's tee function with its intricacies). then, switching back to the streams, you'll be able to appreciate how it is done automatically (and / or better) by the streams, and even see the different choices in the possible streams' implementations.Olgaolguin
O
4

As a matter of proper naming, merge shouldn't be removing duplicates, as its name suggests its being part of mergesort which ought to preserve them. Union is a better name for such operation, working with sets represented (here) by increasing lists of unique numbers, which constraint it ought to preserve by removing the duplicates that can only come from both of its arguments.

Back to the problem itself, let's write it symbolically as

S235 = {1} ∪ 2*S235 ∪ 3*S235 ∪ 5*S235

Premature implementation is the mother of all evil! (wait, what?) We won't even yet try to establish how exactly those s do their job, not even in which order. Or even how many of the terms there are there:

S23 = {1} ∪ 2*S23 ∪ 3*S23

or even

S2 = {1} ∪ 2*S2

Now this looks simple enough. We can even fake-implement the union of A and B here simply as, first, taking all the elements of A, and then -- of B. And it will work just fine, here, because there's only one element in this 's left input:

 {1} ----∪-->--->--S₂--.--->S₂
        /               \        
        \______*2_______/        
          ---<----<---         

How does this work? 1 enters the combiner, exits it first, unconditionally (NB this discovered requirement is important, for if had to examine both of its arguments right away we'd get ourselves an infinite loop, a black hole in Haskell argot), is split in two by the . splitter, then the first copy of 1 continues forth to the output point while the second copy of 1 goes back through the *2 multiplier, the resulting 2 enters back the this time on the right, unopposed by anything on the left (which is at this time already empty), and continues on in the same fashion so 2 goes to the output point, then 4, then 8, etc. etc. (mumble mumble the looping control path of the recursive definition has become the data path looping back onto itself mumble mumble).

To put it differently, S₂ contains all elements of {1}; plus all elements of {1} that went through the *2 multiplier once; and twice; and three times; and so on and so forth -- all the powers of 2 in increasing order:

S2 = {1} ∪ 2*{1} ∪ 2*2*{1}                ;; == {1, 2, 4, 8, 16, 32, ...}
                 ∪ 2*2*2*{1}
                 ∪ 2*2*2*2*{1}
                 ∪ ..........

The two S₂'s in the diagram are the same because whatever we siphon from it at the splitter point does not affect it.

Wasn't this fun?

So how do we go about adding the multiples of 3 to it? One way to do it is

S23 = S2 ∪ 3*S23
 {1} ----∪-->--->--S₂--.---S₂----∪-->--->--S₂₃--.--->S₂₃
        /               \       /                \        
        \______*2_______/       \______*3________/        
          ---<----<---            ---<----<---         

Here 1 from S₂ enters the second combiner and proceeds to the output point S₂₃ as well as back through the *3 multiplier, turning into 3. Now the second has 2,4,8,... and 3,... as its inputs; 2 goes through as well as turning into 6. Next, has 4,8,16,... and 3,6,...; 3 goes through. Next, 4; etc., etc., and so on and so forth.

Thus all elements of S₂ are part of S₂₃, but so are also all elements of S₂ that went through the *3 multiplier once, and twice, etc., -- all the powers of 2 and 3 multiplied together, in increasing order:

S23 = S2 ∪ 3*S2 ∪ 3*3*S2                   ;; = S2 ∪ 3*( S2 ∪ 3*S2 
                ∪ 3*3*3*S2                 ;;               ∪ 3*3*S2 
                ∪ 3*3*3*3*S2               ;;               ∪ 3*3*3*S2 
                ∪ ..........               ;;               ∪ ........ )   !!

Why the increasing order? How? Why, that is the responsibility of ! Hello, another discovered requirement. Whatever enters it on either side, it must produce the smaller element before the larger one.

And what is it to do in the event the two are equal? Do we even need to concern ourselves with this question in this here scheme? Can this ever happen, here?

It can't. And so we can implement the here as a merge, not as a union (but remember the first discovered requirement! -- is it still valid? needed? with the addition of new cases). Merge ought to be more efficient than union as it doesn't concern itself with the case of equals.

And for the multiples of 5 also? We continue, as

S235 = S23 ∪ 5*S235
 {1} ----∪-->--->--S₂--.---S₂----∪-->--->--S₂₃--.---S₂₃----∪-->--->--S₂₃₅--.--->S₂₃₅
        /               \       /                \         /                 \ 
        \______*2_______/       \______*3________/         \_______*5________/ 
          ---<----<---            ---<----<---                ---<----<---     
  • Does this describe the code from the book? _______
  • Does this describe a code which is about twice faster than the one from the book? _______
  • Why is it about twice faster than the code from the book? _______
  • Does this answer your question? _______
  • Does this help you answer your question? _______

(fill in the blanks).

See also:


And the signal processing block diagram for the book's code is:

                                  1 --->---.
                                            \
                                             cons-stream ->-- S ---.---> S
     .---->---->--- *2 --->---.             /                      |
    /                          \           /                       |
   /                            union ->--*                        .
  .-->-- *3 -->--.             /                                  /
  |               \           /                                  /
  |                union ->--*                                  /
  |               /                                            /
  .-->-- *5 -->--*                                            /
   \                                                         /
    *--------<--------<--------<--------<---------<---------*

where the duplicates-removing "union" is called merge in the book, implementing the original formula

S235 = {1} ∪ (2*S235 ∪ (3*S235 ∪ 5*S235))

with a specific ordering, and correspondingly different implementation of ∪.

Olgaolguin answered 23/4, 2019 at 9:24 Comment(9)
Often, it seems to me, the book deals both with quite abstract ideas and the details of their implementation at the same time. This is a really clear view of the problem from 'above' and it's lovely to see a solution evolving from the analysis of the problem. Generally, I'm still looking at problems from 'below', trying to understand the implementation and the magic behind these streams, trying to "learn the trick" of how simple procedures like delay and force can allow such elegant solutions. Which is challenging when I spend my day with 'enterprise' Java. :-)Woald
@Woald yes I understand what you mean. for some reason (educational, perhaps) we (I mean me too, as well as you) are usually imperatively inclined. I too spent great efforts first on the imperative understanding exactly as you describe. Not until I wrote this answer could I as clearly and "simply" explain this to myself. :) if you concentrate first on the S2 example, it can be read both in an imperative i.e. one-by-one fashion, and declarative, i.e. "sets of values" -- because the set {1} has one element! :) the trick is in the merge/union's preserving of the relevant invariant: 1/Olgaolguin
@Woald two ascending sequences in, one ascending sequence out. hence the two possible implementations, the union and the merge, depending on whether some number can be present in both sequences (which are assumed already devoid of duplicates) or not. (merge, of course, can be seen as "two non-descending sequences in, one non-descending sequence out", with exactly the same implementation, but that's not relevant here). If you would like to see something added to this answer, or have a specific question, don't hesitate to ask. /2Olgaolguin
@Woald and actually these code snippets/ diagrams are exact pseudocode descriptions of a working algorithm. You just need to implement the U as I described, which can be almost exactly the same as the merge (which is "union") from the book, with just one crucial difference: they need to pass through the first element of the left input unconditionally, i.e. before pulling the right argument apart into the head and the rest of stream. (Using "union"-like comparisons is just less efficient, here.)Olgaolguin
@WillNess Thanks for putting in the effort, Great answer.Impart
@WillNess I can't thank you enough, I'll surely buy you a beer :DImpart
@Impart heh. maybe one day you'll do, and I'll thank you back. :)Olgaolguin
@WillNess Thank you very much for your offer of help, my focus is now further in the book where, as usual, I'm struggling. But that's what I enjoy - it would be pointless if it were easy. I think answers like yours here are really valuable. As one gets further in the book it is harder to find authoritative answers online. There are some smart people who have written up their answers as they work through the book, but some fall by the wayside and many who persist are those who are learning the most - and so not all their answers are as authoritative as those of someone with more experience.Woald
@Woald well, its now here, and hopefully will be forever, so you can return to it later whenever you feel like it. :) you can google for sites about SICP and its exercises, there's even a wiki of them, IIRC. Happy trails! and whenever you hit a snag, post an SO question!Olgaolguin
W
4

This is my best attempt to visualize it. But I do struggle, it feels like a snake with three heads eating its own tail.

If we say the values of the stream S are s0, s1, s2, ..., then 
initially we only know the first value, s0.

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    ?    ?    ?    ?    ?    ?    ?    ?    ?    ?  

But we do know the three scale-streams will be producing multiples of
these values, on demand:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    ?    ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:   2*1  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:   3*1  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5*1  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Merge will initially select the lowest of the numbers at the heads of
these three streams, forcing their calculation in the process:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    ?    ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:  [2]  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:   3   3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


 So s1 will now have the value 2:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1   [2]   ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:        2*2  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:   3    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Merge will now select 3 as the minimum of 4, 3, and 5:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:        4    2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:  [3]   3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


and will put it into the next slot in the result stream S, s2:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2   [3]   ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:        4    2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:        3*2  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Scale-2's head is selected again:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3   [4]   ?    ?    ?    ?    ?    ?    ?  

    scale-2:             2*3  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:        6    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


And then 5 is selected from scale-5 and placed in the result:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4   [5]   ?    ?    ?    ?    ?    ?  

    scale-2:             6    2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:        6    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        5*2  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Two streams have 6 at their head, both are consumed but only one 6 
is placed in the result:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5   [6]   ?    ?    ?    ?    ?  

    scale-2:                  2*4  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:             3*3  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        10   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


And a few more iterations:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6   [8]   ?    ?    ?    ?  

    scale-2:                       2*5  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:             9    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        10   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8   [9]   ?    ?    ?  

    scale-2:                       10   2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:                  3*4  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        10   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    _________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8    9   [10]  ?    ?  

    scale-2:                            2*6  2*?  2*?  2*?  2*?  2*?
    scale-3:                  12   3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:             5*3  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8    9    10  [12]  ?  

    scale-2:                                 2*8  2*?  2*?  2*?  2*?
    scale-3:                       3*5  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:             15   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    _________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8    9    10   12  [15]

    scale-2:                                 16   2*?  2*?  2*?  2*?
    scale-3:                            3*6  3*?  3*?  3*?  3*?  3*?
    scale-5:                  5*4  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________

So perhaps it's more like a snake with one head taking alternate bites from its three tails.

Woald answered 13/4, 2019 at 14:15 Comment(9)
Thanks a lot. It feels good to know that I'm not the only one who finds this hard to visualise. Great answer, Thanks againImpart
it's not one snake though. there are two of them. each is called merge here. merge works in the small: it doesn't know that its inputs come from what it outputted earlier. it just looks at the two input heads, compares them, pulls the smallest one. the two merges are arranged in a (shortest) tree, so that the output of the lower one comes as the right input of the higher-placed another. Do you think it's worth it if I added the diagrams which follow the book's code exactly, to my answer?Olgaolguin
we do have to make an effort here to "work in the small", to "let go and have faith" exactly like we do with recursion: the whole point of recursion is not to try to see the whole mechanism working, but concentrate on the smaller parts, ensure the invariants are preserved, so that the composition of the smaller parts into one bigger thing is guaranteed to work as well. so in the end both streams as recursion are about compositionality.Olgaolguin
but if you meant it conceptually, then yes! --- btw here you indeed can see one merge I mean snake, biting from its three tails. :) (though it's in Haskell, where : is cons-stream and f x y means ((f x) y)).Olgaolguin
@WillNess Forgive me for brief comments, I'm neither a quick programmer nor a quick writer and my head hurts thanks to chapter 4. I think we are on common ground :-) It's interesting that you say "let go and have faith" this is echoed in the book with reference to magic and spells (hence the wizard on the cover). We should cast spells without worrying how they work. But "Implementation" is in the title too, and (iirc) this exercise is at the point where we might accept stream primitives on faith, but we have only just been introduced to streams that are defined in terms of themselves.Woald
@Woald Exactly my thought. I'm all in for letting go and having faith. But somewhere deep inside me, I have this thought that, Did i really understand it? or did I get lucky with this particular problem. I wonder if anyone else feels this way.Impart
@Impart Absolutely. Especially with SICP, just when you think you have ‘learnt the lesson’ they add a nuance or complication that will expose any weakness in your understanding.Woald
I tweaked it a little bit, see if it's clearer this way. feel free to revert! :)Olgaolguin
Great answer! Well donePartible
O
4

As a matter of proper naming, merge shouldn't be removing duplicates, as its name suggests its being part of mergesort which ought to preserve them. Union is a better name for such operation, working with sets represented (here) by increasing lists of unique numbers, which constraint it ought to preserve by removing the duplicates that can only come from both of its arguments.

Back to the problem itself, let's write it symbolically as

S235 = {1} ∪ 2*S235 ∪ 3*S235 ∪ 5*S235

Premature implementation is the mother of all evil! (wait, what?) We won't even yet try to establish how exactly those s do their job, not even in which order. Or even how many of the terms there are there:

S23 = {1} ∪ 2*S23 ∪ 3*S23

or even

S2 = {1} ∪ 2*S2

Now this looks simple enough. We can even fake-implement the union of A and B here simply as, first, taking all the elements of A, and then -- of B. And it will work just fine, here, because there's only one element in this 's left input:

 {1} ----∪-->--->--S₂--.--->S₂
        /               \        
        \______*2_______/        
          ---<----<---         

How does this work? 1 enters the combiner, exits it first, unconditionally (NB this discovered requirement is important, for if had to examine both of its arguments right away we'd get ourselves an infinite loop, a black hole in Haskell argot), is split in two by the . splitter, then the first copy of 1 continues forth to the output point while the second copy of 1 goes back through the *2 multiplier, the resulting 2 enters back the this time on the right, unopposed by anything on the left (which is at this time already empty), and continues on in the same fashion so 2 goes to the output point, then 4, then 8, etc. etc. (mumble mumble the looping control path of the recursive definition has become the data path looping back onto itself mumble mumble).

To put it differently, S₂ contains all elements of {1}; plus all elements of {1} that went through the *2 multiplier once; and twice; and three times; and so on and so forth -- all the powers of 2 in increasing order:

S2 = {1} ∪ 2*{1} ∪ 2*2*{1}                ;; == {1, 2, 4, 8, 16, 32, ...}
                 ∪ 2*2*2*{1}
                 ∪ 2*2*2*2*{1}
                 ∪ ..........

The two S₂'s in the diagram are the same because whatever we siphon from it at the splitter point does not affect it.

Wasn't this fun?

So how do we go about adding the multiples of 3 to it? One way to do it is

S23 = S2 ∪ 3*S23
 {1} ----∪-->--->--S₂--.---S₂----∪-->--->--S₂₃--.--->S₂₃
        /               \       /                \        
        \______*2_______/       \______*3________/        
          ---<----<---            ---<----<---         

Here 1 from S₂ enters the second combiner and proceeds to the output point S₂₃ as well as back through the *3 multiplier, turning into 3. Now the second has 2,4,8,... and 3,... as its inputs; 2 goes through as well as turning into 6. Next, has 4,8,16,... and 3,6,...; 3 goes through. Next, 4; etc., etc., and so on and so forth.

Thus all elements of S₂ are part of S₂₃, but so are also all elements of S₂ that went through the *3 multiplier once, and twice, etc., -- all the powers of 2 and 3 multiplied together, in increasing order:

S23 = S2 ∪ 3*S2 ∪ 3*3*S2                   ;; = S2 ∪ 3*( S2 ∪ 3*S2 
                ∪ 3*3*3*S2                 ;;               ∪ 3*3*S2 
                ∪ 3*3*3*3*S2               ;;               ∪ 3*3*3*S2 
                ∪ ..........               ;;               ∪ ........ )   !!

Why the increasing order? How? Why, that is the responsibility of ! Hello, another discovered requirement. Whatever enters it on either side, it must produce the smaller element before the larger one.

And what is it to do in the event the two are equal? Do we even need to concern ourselves with this question in this here scheme? Can this ever happen, here?

It can't. And so we can implement the here as a merge, not as a union (but remember the first discovered requirement! -- is it still valid? needed? with the addition of new cases). Merge ought to be more efficient than union as it doesn't concern itself with the case of equals.

And for the multiples of 5 also? We continue, as

S235 = S23 ∪ 5*S235
 {1} ----∪-->--->--S₂--.---S₂----∪-->--->--S₂₃--.---S₂₃----∪-->--->--S₂₃₅--.--->S₂₃₅
        /               \       /                \         /                 \ 
        \______*2_______/       \______*3________/         \_______*5________/ 
          ---<----<---            ---<----<---                ---<----<---     
  • Does this describe the code from the book? _______
  • Does this describe a code which is about twice faster than the one from the book? _______
  • Why is it about twice faster than the code from the book? _______
  • Does this answer your question? _______
  • Does this help you answer your question? _______

(fill in the blanks).

See also:


And the signal processing block diagram for the book's code is:

                                  1 --->---.
                                            \
                                             cons-stream ->-- S ---.---> S
     .---->---->--- *2 --->---.             /                      |
    /                          \           /                       |
   /                            union ->--*                        .
  .-->-- *3 -->--.             /                                  /
  |               \           /                                  /
  |                union ->--*                                  /
  |               /                                            /
  .-->-- *5 -->--*                                            /
   \                                                         /
    *--------<--------<--------<--------<---------<---------*

where the duplicates-removing "union" is called merge in the book, implementing the original formula

S235 = {1} ∪ (2*S235 ∪ (3*S235 ∪ 5*S235))

with a specific ordering, and correspondingly different implementation of ∪.

Olgaolguin answered 23/4, 2019 at 9:24 Comment(9)
Often, it seems to me, the book deals both with quite abstract ideas and the details of their implementation at the same time. This is a really clear view of the problem from 'above' and it's lovely to see a solution evolving from the analysis of the problem. Generally, I'm still looking at problems from 'below', trying to understand the implementation and the magic behind these streams, trying to "learn the trick" of how simple procedures like delay and force can allow such elegant solutions. Which is challenging when I spend my day with 'enterprise' Java. :-)Woald
@Woald yes I understand what you mean. for some reason (educational, perhaps) we (I mean me too, as well as you) are usually imperatively inclined. I too spent great efforts first on the imperative understanding exactly as you describe. Not until I wrote this answer could I as clearly and "simply" explain this to myself. :) if you concentrate first on the S2 example, it can be read both in an imperative i.e. one-by-one fashion, and declarative, i.e. "sets of values" -- because the set {1} has one element! :) the trick is in the merge/union's preserving of the relevant invariant: 1/Olgaolguin
@Woald two ascending sequences in, one ascending sequence out. hence the two possible implementations, the union and the merge, depending on whether some number can be present in both sequences (which are assumed already devoid of duplicates) or not. (merge, of course, can be seen as "two non-descending sequences in, one non-descending sequence out", with exactly the same implementation, but that's not relevant here). If you would like to see something added to this answer, or have a specific question, don't hesitate to ask. /2Olgaolguin
@Woald and actually these code snippets/ diagrams are exact pseudocode descriptions of a working algorithm. You just need to implement the U as I described, which can be almost exactly the same as the merge (which is "union") from the book, with just one crucial difference: they need to pass through the first element of the left input unconditionally, i.e. before pulling the right argument apart into the head and the rest of stream. (Using "union"-like comparisons is just less efficient, here.)Olgaolguin
@WillNess Thanks for putting in the effort, Great answer.Impart
@WillNess I can't thank you enough, I'll surely buy you a beer :DImpart
@Impart heh. maybe one day you'll do, and I'll thank you back. :)Olgaolguin
@WillNess Thank you very much for your offer of help, my focus is now further in the book where, as usual, I'm struggling. But that's what I enjoy - it would be pointless if it were easy. I think answers like yours here are really valuable. As one gets further in the book it is harder to find authoritative answers online. There are some smart people who have written up their answers as they work through the book, but some fall by the wayside and many who persist are those who are learning the most - and so not all their answers are as authoritative as those of someone with more experience.Woald
@Woald well, its now here, and hopefully will be forever, so you can return to it later whenever you feel like it. :) you can google for sites about SICP and its exercises, there's even a wiki of them, IIRC. Happy trails! and whenever you hit a snag, post an SO question!Olgaolguin

© 2022 - 2024 — McMap. All rights reserved.