Ren-C's eloquence in the face of FizzBuzz


#1

So there’s a silly problem called “FizzBuzz” which is stated very simply:

Write a program that prints the numbers from 1 to 100.
But for multiples of three print "Fizz" instead of the number
....and for the multiples of five print "Buzz".
For numbers which are multiples of both three and five print "FizzBuzz".

The claim is that as easy as this problem sounds, many programmers who walk into interviews have trouble with it. On the c2 programming wiki, someone argues for why this might be the case:

“I think Fizz-Buzz is “hard” for some programmers because (#1) it doesn’t fit into any of the patterns that were given to them in school assignments, and (#2) it isn’t possible to directly and simply represent the necessary tests, without duplication, in just about any commonly-used modern programming language.

But the design of Ren-C has more than enough chops for this problem! It’s particularly elegant using a recent change to DELIMIT, including its specializations like SPACED and UNSPACED.

So how about that FizzBuzz?

count-up n 100 [
    print [
         unspaced [
              if n mod 3 = 0 ["Fizz"]
              if n mod 5 = 0 ["Buzz"]
         ] else [n]
    ]
]

Presto…that’s all it takes!!! UNSPACED will return null if everything in its body opts out, which is the cue for the generic ELSE to run. This brings the magic of being able to avoid duplication in the tests. It’s like every piece of problem specification corresponds to just one word in the program!

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
31
32
Fizz
34
Buzz
Fizz
37
38
Fizz
Buzz
41
Fizz
43
44
FizzBuzz
46
47
Fizz
49
Buzz
Fizz
52
53
Fizz
Buzz
56
Fizz
58
59
FizzBuzz
61
62
Fizz
64
Buzz
Fizz
67
68
Fizz
Buzz
71
Fizz
73
74
FizzBuzz
76
77
Fizz
79
Buzz
Fizz
82
83
Fizz
Buzz
86
Fizz
88
89
FizzBuzz
91
92
Fizz
94
Buzz
Fizz
97
98
Fizz
Buzz

Do not COLLECT [keep if false [$100]]
#2

I had been considering the change to DELIMIT for a while, but seeing just what a win for this example put it over the top.

One might have tried and done this from the outside, for instance with a CHAIN:

unspaced*: chain [:unspaced | function [x] [if x <> "" [x]]]

That makes a variant of the old UNSPACED which pipes its output into a function that uses an IF test against empty string…and if it is the empty string the test fails and it returns null, as all failed conditionals do.

While that works (and is impressive in its own right), it isn’t quite as useful. The problem is that this would mean that you’d wind up with a null even if you had purposely put empty strings into your data. But I think that makes a really good way to opt out of the nullification:

>> unspaced [null null]
// null

>> unspaced ["" null]
== ""

Making that distinction is something DELIMIT has to do in its implementation, or you wind up rewriting DELIMIT!

The change to DELIMIT was too compelling to not do!

…because the whole point of the system design is to get bragging rights on problems like FizzBuzz.

I think this is impressive, but it’s really very much the tip of the iceberg of what’s going on.


#3

Love it. FizzBuzz is indeed a neat example of a language challenge.
I’m still inexperienced with unspaced, so its use here seems like a fancy trick but not an intuitive function name.
This is in no way a complaint :wink: , I’m just thinking that someone coming from another language might see this short eloquent example but wonder “Cool-- Wait, what the heck is unspaced?”.


#4

I guess it seems fairly sensible to to me…especially when people get introduced to it as print unspaced […]. If that’s how you learn it, you then think of being able to lift out the unspacing behavior you used when you needed it into a function. And you can say print spaced […] too, though that’s usually redundant.

Though if it were just JOIN it might appear clearer in this particular case, I think reserving JOIN for something else makes sense. Trying to use a generic series-joining operator for string production is problematic, as @Brett explains in detail.

But it’s a good point that presenting FizzBuzz probably needs a little lead-in to explain UNSPACED to the uninitiated, to make absorption smoother.


#5

Yes, I must relearn some of my old ways… I still think in terms of the JOIN/REJOIN vernacular. JOIN is a nice verb which intuitively suggests its purpose. UNSPACED on the other hand, seems defined in contrast to its opposite, which is also less obvious. To a dim bulb like me, anyway. Still not a complaint; I think with more use of REN-C I won’t blink at some of the nuanced changes.

By the way, we should ask @BrianOtto to include this script example on the Rebol home page.


#6

I sent him an email via a forum-oriented link about the online demo, to tell him about that. But someone might need to chase him down in another medium…I know not everyone gets those mails for one reason or another.

I think that contrast definition will make more sense to people who learn PRINT first…realize PRINT is powered by SPACED…and then realize that UNSPACED is their alternative to what Rebol thinks of as the “natural” default.

Speaking of PRINT, this change involves a change to PRINT to make sure print [] doesn’t give an error, by adding a TRY:

print: function [
    {Textually output value (evaluating elements if a block), adds newline}

    return: "null if blank input, otherwise void"
        [<opt> void!]
    line "Line of text, or block to run SPACED on - blank prints nothing"
        [blank! text! block!]
][
    write-stdout switch type of line [
        blank! [return null] // don't print the newline
        text! [line]
        block! [try spaced line] // blank if all opt-out, so WRITE-STDOUT is no-op
    ]
    write-stdout newline
]

One interesting thing here is that it shows a good reason for why cases that error on NULL from things like SPACED and UNSPACED on empty input is a good thing. If you say:

 print unspaced [... bunch of stuff ...]

And UNSPACED comes back finding that all that stuff opted out and you’ve got nothing, which behavior do you want: an empty line, or no line at all? Having NULL as the result and trigger an error helps you get in there if this case comes up, and be clear about it.

print (unspaced [...] else [""]) // (a) empty line if all opt-out
print <- unspaced [...] else [""] // variant of (a) using precedence manipulation
print unspaced ["" ...] // variant of (a)...forces non-null UNSPACED result
print <- "" unless (unspaced [...]) // another (a), so many options!

print try unspaced [...] // (b) no output (including no newline) if all opt-out

Since ELSE completes its left hand side, you have to use a GROUP! (or an <-) to cluster the unspaced with the else…otherwise it will interpret it as (print unspaced [...]) else [""]. But a second interesting note is that PRINT now follows “blank in, null out”, so having an ELSE attached to a print can now have meaning…such as in the second case!

print try unspaced [...] else [
    // code to run if all the unspaced content opted out, hence
    // blank input was given to print, and no output (including
    // no newline) was printed...this way you can react to the
    // case of no output (if you just wanted an error, leave off
    // the TRY!)
]

That’s because VOID!s are values (albeit quite unfriendly ones) so they doesn’t count for triggering the ELSE.

There’s quite a lot of design here, and I still feel that this language is going to be quite the code golf weapon!


#7

Thanks for the remedial education. This is the kind of stuff which will be crucial important to nail-down for documentation. There are usually a few options to handle each flow of logic, due to VOID, NULL, BLANK, etc. and this influences the way the code is commonly expressed, and the language idioms. All good stuff, but I think old Rebol papered-over much of this complexity so the semantics would appear simpler. And as a result some of this important logic may have been half-baked for those who want a more complete and serious/rigorous language.


#8

I knew about spaced/unspaced, had not seen it in action yet. I think unspaced here was very intuitive, like it very much, it is a true Rebol way solution. (And totally unmatched anywhere else afaik).