- Tour Clojure concepts & stdlib via FizzBuzz
- Every FizzBuzz must have reason to exist
- Plan A: Rapid-fire live demo, until timer runs out
- Plan B: Blog post has all the stuff I will skip
- Plan C: If #demofail, will record separately
- Tweet this. Look smart. Get paid!
“If you do FP right, you get OOP for free. And vice-verca.” #Clojure #OCaml #Smalltalk #Erlang \m/
O Lambda the Ultimate, bless we who are in this demo.
That our core be functional, and our functions be pure.
That our data be immutable, so we may know the value of values.
That our systems be composable, so they may grow and scale with grace.
That their States only mutate in pleasantly surprising ways.
That our networks & computers work, at least for 20 more minutes.
For otherwise nothing lives. Nothing evolves.
In the name of the alpha, the beta, and the eta…
(λx.x x) (λx.x x)
Fizz buzz is a group word game for children to teach them about division.
Players take turns to count incrementally,
replacing any number divisible by THREE
with
the word “Fizz”, and any number divisible by
FIVE
with the word “Buzz”.
- Wikipedia
Natural Numbers, starting at 1.
A postmodern revival of Latin roots of American English.
complect : braided, entwined, hopelessly complected : thing that makes Clojurian frown
decomplect : unbraid, disentwine decomplected : thing that makes Clojurian smile
decomplecting : activity that Clojurians enjoy what we will try to do now
- Accidentally discover
for
in stdlib - “Ooh, List Comprehension. Nice!” (- The Python gentlenerds in the house :)
(ns user)
(defn fizz-buzz-classic
[num-xs]
(for [n num-xs]
(cond
(zero? (rem n 15)) (println "FizzBuzz")
(zero? (rem n 3)) (println "Fizz")
(zero? (rem n 5)) (println "Buzz")
:else (println n))))
Those pesky nils, tho.. (see REPL console)
for
is Lazy- REPL is eager
- Here be Dragons
- Unlearn old habits
- Or learn from prod outage
- Behold cleaned up version
(defn lazybuzz
[num-xs]
(for [n num-xs]
(cond
(zero? (rem n 15)) "FizzBuzz"
(zero? (rem n 3)) "Fizz"
(zero? (rem n 5)) "Buzz"
:else n)))
println
is no more, nils are no more(lazybuzz (list 1 3 5 15 16))
- The Shape of Nils
(fizz-buzz-classic (list 1 3 5 15 19))
- The Shape of Nils
“Classic” FizzBuzz considered harmful (in Clojure) Examine & avoid its severe defects:
- Broken behaviour
calculations functional, but
println
non-deterministic - Broken API contract “Classic” returns useless nils. We like useful values.
- Broken time model Never mix Effects (“do NOW”) with Laziness (“maybe never”) Define separately, join later in safe ways
- Broken aesthetic Do one job, do it well. Printing is second job. “That’s George’s problem.” - Hal & Gerry
(Bonus: See blog post for ideas to get fired with fizzbuzz.)
- Keep your fns pure, like
lazybuzz
- Laziness becomes friend, as nice bonus!
(Recall the children’s game definition)
(def all-naturals (rest (range))) (def all-fizz-buzzes (lazybuzz all-naturals))
- Let REPL print. Separately.
(take 15 all-fizz-buzzes)
Little functions are good!
(zero? (rem n divisor))
can be let
-bound fn
(defn letbuzz
[num-xs]
(for [n num-xs]
(let [divisible? (fn [n1 n2] (zero? (rem n1 n2)))] ; locally-bound lambda
(cond
(divisible? n 15) "FizzBuzz"
(divisible? n 3) "Fizz"
(divisible? n 5) "Buzz"
:else n))))
BUT, distinct idea, so define it separately
(defn divisible?
"True when the remainder of n1/n2 is zero. e.g. (divisible? 4 2) => true"
[n1 n2]
(zero? (rem n1 n2)))
(def divisible-2?
(comp zero? rem))
All are refrentially transparent. Tiny, but add compositional firepower.
(defn basic-buzz
[n]
(cond
(divisible? n 15) "FizzBuzz"
(divisible? n 3) "Fizz"
(divisible? n 5) "Buzz"
:else n))
(basic-buzz 1)
(basic-buzz 3)
(basic-buzz 5)
(basic-buzz 15)
(def all-fizz-buzzes
(for [n (rest (range))]
(basic-buzz n)))
(def all-fizz-buzzes
(map basic-buzz (rest (range))))
reduce
is your homework…
Almost too embarrassing to write…
(ns user)
(def fizz-buzz map)
(def par-buzz pmap)
Get CPU-bound parallelism trivially…
(= (fizz-buzz basic-buzz (range 1 101))
(par-buzz basic-buzz (range 1 101)))
Not too hard to understand!
(clojure.repl/source pmap)
Open up design space even more with FizzBuzz Biz domain concepts
Note:
- reordered argument list more constant -to-> more variable
- “truthiness” implies yes/no
(defn divisible?
"Given a number 'n', return the given word (truthy) when it is divisible
by the divisor, or nil otherwise (falsey)."
[divisor the-word n]
(when (zero? (rem n divisor))
the-word))
(def fizzes?
"Is a given number divisible by 3?"
(partial divisible? 3 "Fizz"))
(def buzzes?
"Is a given number divisible by 5?"
(partial divisible? 5 "Buzz"))
(def fizzbuzzes?
"Is a given number divisible by 3 and 5?"
(partial divisible? 15 "FizzBuzz"))
(defn or-buzz
"Sadly, order of conditionals still matters."
[n]
(or (fizzbuzzes? n)
(buzzes? n)
(fizzes? n)
n))
This use of juxt
too subtle for production
BUT we use it usefully later
(defn juxt-buzz
"((juxt f g h) 42) -> [(f 42) (g 42) (h 42)]"
[n]
(some identity ((juxt fizzbuzzes? buzzes? fizzes? identity)
n)))
Make order of calculation not matter. 15 is LCM. Play with remainders of 15.
(def rem15->fizz-buzz
"A table of remainders of 15, in a hash-map."
{0 "FizzBuzz"
3 "Fizz"
6 "Fizz"
9 "Fizz"
12 "Fizz"
5 "Buzz"
10 "Buzz"})
(rem15->fizz-buzz (rem 3 15))
;; ~nil~ implies "no result found"
(rem15->fizz-buzz (rem 1 15))
nil
-pun with short-circuiting or
(defn or-rem15-buzz
[n]
(or (rem15->fizz-buzz (rem n 15))
n))
But get
is more right, with fallback for “not found”
(defn get-rem15-buzz
[n]
(get rem15->fizz-buzz
(rem n 15)
n))
FizzBuzz cyclical in modulo 3, 5, and 15.
(def mod-cycle-buzz
"We can declare a lazy sequence of FizzBuzz as modulo 3, 5, 15.
The sequence is ordered by definition."
(let [n identity
f (constantly "Fizz")
b (constantly "Buzz")
fb (constantly "FizzBuzz")]
(cycle [n n f
n b f
n n f
b n f
n n fb])))
Map can operate over n
collections.
(def all-fizz-buzzes
(map (fn [f n] (f n))
mod-cycle-buzz ; countless modulo pattern
(rest (range)))) ; countless sequence of numbers
(defn any-mod-cycle-buzz
[num & words]
(or (not-empty (reduce str words))
num))
(map any-mod-cycle-buzz
(range 1 16)
(cycle [nil nil "Fizz"])
(cycle [nil nil nil nil "Buzz"])
;; bring on all the primes!
(cycle [nil 'Biz])
(cycle [nil nil nil nil nil nil 'Fuz]))
I of + is 0 I of * is 1 I of FizzBuzz is… the nums as-is. Naturally.
(map any-mod-cycle-buzz
(range 1 16))
PeanoBuzz number system starts at [0 0]
(def S
"PeanoBuzz is closed under this definition of Successor."
(comp (juxt identity get-rem15-buzz)
inc
first))
(def all-peano-buzzes
(iterate S [0 0]))
FizzBuzz as Peano numbers
(= (take 16 all-peano-buzzes)
[[0 0] [1 1] [2 2]
[3 "Fizz"] [4 4]
[5 "Buzz"]
[6 "Fizz"] [7 7] [8 8]
[9 "Fizz"]
[10 "Buzz"] [11 11]
[12 "Fizz"] [13 13] [14 14]
[15 "FizzBuzz"]])
Trivially map PeanoBuzz back into Standard FizzBuzz
(= (fizz-buzz basic-buzz
(range 1 101))
(fizz-buzz second
(take 100 (rest all-peano-buzzes))))
<-- ------- MECHANISM -------- -->|<-- POLICY -->
| n divisible? 3 | n divisible? 5 | Final value |
|----------------+----------------+-------------|
| true | true | FizzBuzz |
| true | false | Fizz |
| false | true | Buzz |
| false | false | n |
(ns dispatch.buzz)
(defn mechanism
"Given two functions, presumably of anything -to-> Boolean, return
a function that can construct inputs of a 2-input truth table."
[f? g?]
(juxt f? g?))
(ns dispatch.buzz)
(defn divisible?
[divisor n]
(zero? (rem n divisor)))
(def fizzes? (partial divisible? 3))
(def buzzes? (partial divisible? 5))
Now this truth table is concretely specific to FizzBuzz.
(map (mechanism fizzes? buzzes?)
[15 3 5 1])
(ns dispatch.buzz)
(def fizz-buzz map)
(def fizz-buzz-mechanism
(mechanism fizzes? buzzes?))
(defmulti dispatch-buzz
"It yields the third column of the truth table."
fizz-buzz-mechanism)
;; The /Policy/, fully realised.
;; ((mechanism fizzes? buzzes?) n) -> final results
(defmethod dispatch-buzz [true true]
[n]
"FizzBuzz")
(defmethod dispatch-buzz [true false]
[n]
"Fizz")
(defmethod dispatch-buzz [false true]
[n]
"Buzz")
(defmethod dispatch-buzz :default
[n]
n)
(fizz-buzz dispatch-buzz
[1 3 5 15 16])
- Name (Class name / Java type)
- Structure (Class members, methods etc.)
- Behaviour (effects caused by methods)
- State (contained in the run-time instance of the Class)
(ns oops.fizzbuzz)
(def divisible? (comp zero? rem))
(def fizz-buzz map)
;; Like a Java Interface, but better.
(defprotocol IFizzBuzz
(proto-buzz [this]))
;; We can add new behaviour to existing types,
;; including /any/ Java built-in type.
(extend-protocol IFizzBuzz
java.lang.Number
(proto-buzz [this]
(cond
(divisible? this 15) "FizzBuzz"
(divisible? this 3) "Fizz"
(divisible? this 5) "Buzz"
:else this)))
(fizz-buzz proto-buzz [1 3 5 15 16])
(fizz-buzz proto-buzz [1.0 3.0 5.0 15.0 15.9])
(ns oops.fizzbuzz)
(= 42 42) ; => true (long and long)
(= 42 42.0) ; => false (long and double)
(= 42.0 42.0) ; => true (double and double)
;; False, as it should be.
(= (fizz-buzz proto-buzz
[1 3 5 15 16])
(fizz-buzz proto-buzz
[1.0 3.0 5.0 15.0 15.9]))
Clojure cleanly solves the ”Expression Problem”.
All fizz-buzzes except PeanoBuzz lose information This is Very Very Bad. Can’t undo entropy.
Records give us all the features of generic hash-maps for free.
(ns boxed.fizz.buzz)
(def divisible? (comp zero? rem))
(def fizz-buzz map)
(defrecord Fizz [n])
(defrecord Buzz [n])
(defrecord FizzBuzz [n])
(defrecord Identity [n])
(defn boxed-buzz
[n]
(cond
(divisible? n 15) (->FizzBuzz n)
(divisible? n 3) (->Fizz n)
(divisible? n 5) (->Buzz n)
:else (->Identity n)))
(def all-boxed-buzzes
(map boxed-buzz
(rest (range))))
(comment
;; Composite hash-map-like data!
(= (fizz-buzz boxed-buzz
[1 3 5 15])
[#boxed.fizz.buzz.Identity{:n 1}
#boxed.fizz.buzz.Fizz{:n 3}
#boxed.fizz.buzz.Buzz{:n 5}
#boxed.fizz.buzz.FizzBuzz{:n 15}]) ; => true
;; Which is nondestructive!
(= [1 3 5 15]
(fizz-buzz (comp :n boxed-buzz)
[1 3 5 15])) ; => true
;; And which has real Java types!
(= (map type (fizz-buzz boxed-buzz [1 3 5 15]))
[boxed.fizz.buzz.Identity
boxed.fizz.buzz.Fizz
boxed.fizz.buzz.Buzz
boxed.fizz.buzz.FizzBuzz]) ; => true
)
off-label use of Clojure Spec’s conform
as a parser.
Skirts the “can be a very bad idea” territory. YMMV.
(ns conformer.buzz)
(require '[clojure.spec.alpha :as s])
(defn divisible?
[divisor n]
(zero? (rem n divisor)))
(def fizzes? (partial divisible? 3))
(def buzzes? (partial divisible? 5))
(s/def ::number number?)
(s/def ::fizzes (s/and ::number fizzes?))
(s/def ::buzzes (s/and ::number buzzes?))
(comment
;; Now we can parse input data...
(s/conform ::fizzes 3) ; => 3
(s/conform ::buzzes 5) ; => 5
(s/conform ::buzzes 3) ; => :clojure.spec.alpha/invalid
(s/conform (s/and ::fizzes ::buzzes) 15) ; => 15
;; And we can handle non-conforming data gracefully,
;; instead of panicking and throwing exceptions.
(s/conform (s/or ::fizzes ::buzzes) "lol") ; :clojure.spec.alpha/invalid
)
(def fizz-buzz-specs
"Set of FizzBuzz parsers."
#{::fizzes ::buzzes ::number})
(defn spec-parse-buzz
"Conform the given input to the set of specs for fizz-buzz, and return
a pair of the input and a map of parsed values keyed by the parser name."
[x]
[x (zipmap fizz-buzz-specs
(map #(s/conform % x) fizz-buzz-specs))])
(def all-spec-buzzes
(map spec-parse-buzz
(rest (range))))
(comment
;; And we can...
(take 100 all-spec-buzzes)
;; Which gives us enriched data, like this:
(= (into {} (map spec-parse-buzz [1 3 5 15 "lol"]))
{1 #:conformer.buzz{:fizzes :clojure.spec.alpha/invalid,
:buzzes :clojure.spec.alpha/invalid,
:number 1},
3 #:conformer.buzz{:fizzes 3,
:buzzes :clojure.spec.alpha/invalid,
:number 3},
5 #:conformer.buzz{:fizzes :clojure.spec.alpha/invalid,
:buzzes 5,
:number 5},
15 #:conformer.buzz{:fizzes 15,
:buzzes 15,
:number 15},
"lol" #:conformer.buzz{:fizzes :clojure.spec.alpha/invalid,
:buzzes :clojure.spec.alpha/invalid,
:number :clojure.spec.alpha/invalid}}) ; => true
)
@rdivyanshu said to add this extra pprint dispatcher, “and let no number escape fizzbuzzness when showing itself”.
Why not?
(ns pprint.buzz)
(require '[clojure.pprint :as pp])
(defn pprint-buzz
[n]
(let [divisible? (comp zero? rem)
prettyprint (comp prn
(partial format "%d doth %s"))]
(cond
(divisible? n 15) (prettyprint n "FizzBuzzeth")
(divisible? n 3) (prettyprint n "Fizzeth")
(divisible? n 5) (prettyprint n "Buzzeth")
:else (prettyprint n "not Fizzeth nor Buzzeth. Alas!"))))
(comment
;; lol
(#'pp/use-method pp/simple-dispatch java.lang.Number pprint-buzz)
;; nothing to see here... you will have to look at the REPL
(doseq [n [1 3 5 15]]
(pp/pprint n)) ;; lol lol lol lolllll
)
A true gentlenerd and a scholar. Nondestructive, and hilarious to boot!
(whatever, input -> whatever) -> (whatever, input -> whatever)
“Seems like a good project for the bar, later on.”
- Rich Hickey
- Data source (sequence, stream, channel, socket etc.)
- Data sink (sequence, stream, channel, socket etc.)
- Data transformer (function of any value -> any other value)
- Data transformation process (mapping, filtering, reducing etc.)
- Some process control (we can transduce finite data (of course) as well as streams, and also have optional early termination in either case. I’m not sure about first-class support for other methods like backpressure.)
(ns transducery.buzz)
(def divisible?
(comp zero? rem))
(defn basic-buzz
[n]
(cond
(divisible? n 15) "FizzBuzz"
(divisible? n 3) "Fizz"
(divisible? n 5) "Buzz"
:else n))
(ns transducery.buzz)
;; Separately define /only/ the transformation "xform"
(def fizz-buzz-xform
(comp (map basic-buzz)
(take 100))) ;; early termination
;; Separately define /only/ input data
(def natural-nums
(rest (range)))
;; Compose to produce a sequence
(comment
(transduce fizz-buzz-xform ;; calculate each step
conj ;; and use this output method
[] ;; to pour output into this data structure
natural-nums) ;; given this input
)
;; Compose differently to produce a string
(comment
(transduce fizz-buzz-xform ;; calculate each step
str ;; and use this output method
"" ;; to catenate output into this string
natural-nums) ;; given this input
)
;; Compose still differently to produce a CSV string
(defn suffix-comma
[s]
(str s ","))
(comment
(transduce (comp fizz-buzz-xform
(map suffix-comma)) ;; calculate each step
str ;; and use this output method
"" ;; to catenate output into this string
natural-nums) ;; given this input
)
Pause for a bit.
- Consider the parts we did not have to modify at all even though we modified everything about the output and about the xform.
- Consider what it might take to reuse any of the other fizzbuzzers instead
of
basic-buzz
. - Try it!
(ns transducery.buzz)
;; Setup
(def numbers-file
"Plaintext file containing numbers in some format."
"/tmp/deleteme-spat-by-clj-fizz-buzz-demo.txt")
;; Write 10,000 natural numbers to file, one per line
#_(spit numbers-file
(clojure.string/join "\n" (range 1 10001)))
;; Read back to double-check we got it.
#_(slurp numbers-file)
;; For contrast: This is how we might fizz-buzz traditionally.
(comment
;; Like this, if we don't know our threading macros.
;; (Don't fret about it. This is just fine.)
(let [fizz-buzz (fn [s] (basic-buzz (Integer/parseInt s)))]
(take 15
(map fizz-buzz
(clojure.string/split-lines (slurp numbers-file)))))
;; Or more Clojurishly, with our nifty threading macros.
(->> numbers-file
slurp
clojure.string/split-lines
(map #(basic-buzz (Integer/parseInt %)))
(take 15))
;; I interrupted us with this, because of a pet peeve. People like to
;; describe this form as a "pipeline". It isn't. It is a formatting
;; sleight of hand that makes in-process call stacks of functions
;; /appear/ to be straight-line. The resulting shape visually suggests
;; punching data through a pipeline.
;;
;; Whereas pipelines are fundamentally streaming abstractions that
;; cross process boundaries.
;;
;; Transducers + xforms are highly pipeline-like.
)
;; Apart from not really being pipelines, both these traditional versions
;; are also hopelessly complected with sequences, which malady is addressed
;; by this transducer version.
(comment
(transduce (comp (map #(Integer/parseInt %))
fizz-buzz-xform) ;; calculate each step
conj ;; and use this output method
[] ;; to pour output into this data structure
(clojure.string/split-lines
(slurp numbers-file))) ;; given this input
)
A reader may complain that split-lines and file slurpin’ is still complected. The reader would be right. Tim Baldridge’s video series listed below will help work out how one might go about transducing over numbers-file directly.
(ns transducery.buzz)
;; The xform can still calculate just a single item:
((fizz-buzz-xform conj) [] 3) ;; => ["Fizz"]
((fizz-buzz-xform str) "" 3) ;; => "Fizz"
((fizz-buzz-xform str) "" 1) ;; => "1"
((fizz-buzz-xform (fn [_ out] out)) nil 3) ;; "Fizz"
((fizz-buzz-xform (fn [_ out] out)) nil 1) ;; 1
Hopefully now it is a little more obvious why the transducer’s mandate of a la carte re-composition demands that all the new pulling apart must be fully compatible with all the old pulling apart.
Thanks to everyone who reviewed drafts and gave feedback and ideas…
friends, fellow Clojurian Slack-ers, sundry gentlenerds, and one’s better half.
May the Source be with you
<3
https://www.evalapply.org/posts/n-ways-to-fizzbuzz-in-clojure/
fizzbuzz@evalapply.org
λ
_\//
Ideas on deck, to put self on the hook…
- [ ] curried fizzbuzz (like Ring libraries),
- [ ] concurrent fizzbuzz (with agents)
- [ ] advanced transducing fizzbuzz (xform all the fizz-buzzes in all ways)
- [ ] Maaaybe re-do Rich’s ants sim 4 species of, ah, ConcurrAnts: FizzAnt, BuzzAnt, FizzBuzzAnt, IdentiAnt
- [ ] Outside of clojure.core? maaybe core.async if not too contrived