Clojure Fizzbuzz

15 views
Skip to first unread message

Paul Barry

unread,
Oct 28, 2008, 7:38:43 AM10/28/08
to novalanguages
Hey Guys,

I whipped up a version of fizzbuzz for Clojure. This version is a
more robust version of the fizzbuzz we were working on in tonight's
meeting. I think this example illustrates a number of nice features
in the language, and since I know some of you are interested in
Clojure, I thought I would write up an explanation.

So there are 3 functions that I've used to implement fizzbuzz. I'll
start with the easy one:

(defn divisible-by?
"This returns true if x is divisible by y, returns false
otherwise"
[x y]
(zero? (rem x y)))

This is a pretty simple Clojure function definition. We are defining
a function called divisible-by? In Clojure, have the function name,
you can optionally include a string that is the documentation for this
function. You can access this function in real-time in the REPL. So
as soon as you define this function, you can do this:

(doc divisible-by?)

And you get the documentation for the function. The arguments to the
function are defined inside of square brackets. Clojure uses square
brackets just to make it stand out from the other parenthesises a
little. Also, Square brackets are the literal syntax for a vector/
array and are commonly used throughout Clojure when you have a list of
things. After that is the body of the function. So next is:

(defn divisible-by-words
"Takes a number and a word map. The word map should have
integers for keys and the values should be the string that the integer
maps to. This will return a string of all the words that that x maps
to, concatenated together. If x does not map to any words, this will
return x. If no word-map parameter is passed, or nil is password for
word-map, this will use the default word-map of 3 'fizz' 5 'buzz'"
([x] (divisible-by-words x (array-map 3 "fizz" 5 "buzz")))
([x word-map]
(if (nil? word-map)
(divisible-by-words x)
(let [words
(apply str
(map #(if (divisible-by? x (first %))
(last %) nil) word-map))]
(if (= words "") x words)))))

You can read the doc string to see what this does. Essentially, it
does the fizz/buzz mapping, but also allows you to pass in a different
mapping. Clojure has literal syntax for map, which is {3 "fizz"}.
This is like a Java HashMap in that it does not preserve the insertion
order of the key/value pairs. array-map gives you a map that does
preserve the order.

This function shows how Clojure allows for the method to take varying
numbers of arguments. If you call this function with one argument,
the ([x] ...) part it what gets evaluated. If you call this function
with 2 arguments, the ([x word-map] ...) part gets evaluated.

Another Clojure feature in there is syntax for anonymous functions.
You can define a function that takes 2 arguments as (fn [x y] (...)),
but you can instead use #(...), and refer to the first and second
arguments as %1 and %2, or the whole list of arguments as %. The
shorter function syntax is commonly used in conjunction with functions
that take functions arguments, like map.

This is mapping over a map (hashtable), so it passes a 2 element
vector of [key value] each time the anonymous function is called.

let is used to define variables that are available while the body of
the let is being evaluated.

So now to the actual fizzbuzz function:

(defn fizzbuzz [& args]
(let [{:keys [min max word-map accumulator] :or {min 1 max 100
word-map nil accumulator nil}} (apply hash-map args)]
(loop [i min acc accumulator]
(if (> i max)
acc
(let [word (divisible-by-words i word-map)]
(if (nil? acc)
(do (prn word)
(recur (inc i) nil))
(recur (inc i) (conj acc word))))))))

First, ignore all the & args, :keys, apply hash-map syntax at the
beginning. I going to write a macro to turn that into this:

(defk fizzbuzz {min 1, max 100, word-map nil, accumulator nil}
(loop [i min acc accumulator]
(if (> i max)
acc
(let [word (divisible-by-words i word-map)]
(if (nil? acc)
(do (prn word)
(recur (inc i) nil))
(recur (inc i) (conj acc word)))))))

Which is a function with keyword arguments that have default values.
Before I explain how this function works, I'll explain how you use it.

First, if you just evaluate (fizzbuzz), then you will see the numbers,
"fizz"s and "buzz"s printed out. Then you can evaluate (fizzbuzz :min
5 :max 55), and have it only do from 5 to 55. Then, you can do
(fizzbuzz :word-map (array-map 4 "foo" 7 "bar")) and it will run with
the rules of divisible by 4 means "foo" and divisible by 7 means
"bar". If you don't care which order the foo and the bar appear in,
you could simply do (fizzbuzz :word-map {4 "foo" 7 "bar"}). Finally,
you can evaluate (fizzbuzz :accumulator []), and instead of printing
the values, if will return a vector of the values. If you evaluate
(fizzbuzz :accumulator ()), you will get the values in a list, but in
reverse order. You can use any combination of these keyword
arguments, in any order.

The other thing of note is the use of loop and recur. This is used to
do recursive operations like this without having to define an extra
functions. The vector that is the first argument to loop is the
binding of values to be used within the loop body. So i is set to min
and acc is set to accumulator. You must pass the new values for i and
acc when you call recur. That makes evaluation go back up to the
loop. Once i exceeeds the max, the function terminates.

The way the accumulator works is by using Clojure's conj function,
which is like Lisp's cons, but "does the right thing", depending on
the data type. It is fast to add something to the front of the list,
and slower to add something to the end. Vectors are the opposite, you
can add to the end fast, slower to add to the front. This isn't
specific to Clojure, it's just the way those data structures work in
any language. So conj adds the item to the front if it is conjing on
a list, to the end if is conjing on a vector. This is part of how
clojure provides a unified method of handling different type of data
structures, which are collectively called sequences in Clojure.

Because most functions in Clojure can be written to the sequence
interface, not really caring if it gets a List or a Vector, you are
free to use Vectors pretty much anywhere. Other advantages of Vectors
are:

1. Vectors don't evaluate the first item in the vector as a function,
so you can write [1 2] instead of having to write '(1 2) to make the
equivalent list.

2. Vectors grow on the end, which is usually what you want, so you
can avoid having to reverse the list or iterate through the range
backwards to get the list to be in the order you want.

3. Vectors also have fast lookup for any given index. This means
getting the 1 millionth element out of a vector with 1 million and 1
elements is fast, not so with lists.

4. Maybe this is subjective, but I feel like using vectors gives the
appear to be easier to read, because there are less parens. For
example, in Clojure, a function that takes 3 arguments and returns a
map with the first argument as the key and the other 2 arguments in a
collection as the value:

(fn foo [x y z] {x [y z]})

In Scheme, it's something like:

(define (foo x y z) (hashtable '(x '(y z))))


So anyway, that's a pretty long-winding, rambling introduction into
some of the things that I think make Clojure a great Lisp, but I have
to qualify that by saying I haven't really used Scheme, Common Lisp or
any other Lisp dialect all that much. I didn't even get into some of
the big things about Clojure, like running on the JVM, persistent data
structures, functional programming, etc. In addition to those things,
there are lots of little things that add up and make Clojure a fun
language to work with.

Here's a link to the full Clojure fizzbuzz:

http://github.com/pjb3/sicp/tree/master/novalang/2008-10-27/fizzbuzz.clj



Dennis

unread,
Nov 24, 2008, 4:06:36 PM11/24/08
to novalanguages
Factor spoiler warning

This isn't nearly as general as Paul's solution, but I think it gets
the idea across.

Dennis

! Copyright (C) 2008 <your name here>
! See http://factorcode.org/license.txt for BSD license.
! Monday, November 24, 2008

USING: kernel math io prettyprint sequences ;
IN: fizzbuzz

: divs? ( n d -- b ) mod 0 = ;
: fb-helper2 ( n -- )
[ 3 divs? dup [ "fizz" write ] when ]
[ 5 divs? dup [ "buzz" write ] when ]
[ -rot or [ nl drop ] [ . ] if ] tri
;
: fizzbuzz ( n -- ) [ 1+ fb-helper2 ] each ;
Reply all
Reply to author
Forward
0 new messages