Skip to content

Instantly share code, notes, and snippets.

@adityaathalye
Last active April 5, 2022 05:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adityaathalye/398d6fc4c8ce9d23e2e393425f0b6454 to your computer and use it in GitHub Desktop.
Save adityaathalye/398d6fc4c8ce9d23e2e393425f0b6454 to your computer and use it in GitHub Desktop.
n ways to fizzbuzz in Clojure (presentation version)

n ways to FizzBuzz in Clojure

Intro

  • 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/

Appease Demo Gods

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)

Definitions

FizzBuzz

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

“Numbers”

Natural Numbers, starting at 1.

Clojurish

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

Le FizzBuzz Classique

  • 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)

… est mort à Clojure. Désolé :(

  • for is Lazy
  • REPL is eager
  • Here be Dragons
  • Unlearn old habits
  • Or learn from prod outage

… remedied

  • 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))
              

… dissected

“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.)

… resurrected, the Clojure way

  • 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)
        

decomplect functionality

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.

decomplect map reduce for

decomplect sequence-making v/s choice-making

(defn basic-buzz
  [n]
  (cond
    (divisible? n 15) "FizzBuzz"
    (divisible? n 3) "Fizz"
    (divisible? n 5) "Buzz"
    :else n))

to get usable standalone logic

(basic-buzz 1)
(basic-buzz 3)
(basic-buzz 5)
(basic-buzz 15)

composable with for

(def all-fizz-buzzes
  (for [n (rest (range))]
    (basic-buzz n)))

and with more design space

(def all-fizz-buzzes
  (map basic-buzz (rest (range))))

reduce is your homework…

decomplect parallelism

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)

decomplect solution domain (code logic)

Open up design space even more with FizzBuzz Biz domain concepts

Pull out 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"))

now we can do ‘or’ buzz

(defn or-buzz
  "Sadly, order of conditionals still matters."
  [n]
  (or (fizzbuzzes? n)
      (buzzes? n)
      (fizzes? n)
      n))

juxt express your choice

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)))

decomplect problem domain (LCM)

Pull out the school maths

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"})

Maps are functions too!

(rem15->fizz-buzz (rem 3 15))
;; ~nil~ implies "no result found"
(rem15->fizz-buzz (rem 1 15))

recompose maths and code

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))

decomplect problem domain more (modulo arithmetic)

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

decomplect ALL the FizzBuzzes (prime factors)

… think prime factors and modulo cycles

(defn any-mod-cycle-buzz
  [num & words]
  (or (not-empty (reduce str words))
      num))

… and variadic map

(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]))

… plus get identity of FizzBuzz for free

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))

decomplect number representation and calculation

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))))

decomplect mechanism and policy

Classically, “mechanism” and “policy” hard-wired together

<-- ------- MECHANISM -------- -->|<-- POLICY -->

| n divisible? 3 | n divisible? 5 | Final value |
|----------------+----------------+-------------|
| true           | true           | FizzBuzz    |
| true           | false          | Fizz        |
| false          | true           | Buzz        |
| false          | false          | n           |

Pry apart mechanism

(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?))

Pry apart policy

(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])

recompose mechanism and policy a-la-carte

(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)

Yes, ‘tis a wee FizzBuzz interpreter!

(fizz-buzz dispatch-buzz
           [1 3 5 15 16])

decomplect OOP

Classical Objects complect…

  • 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)

Clojure Polymorphism fully decomplects it

(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)))

Java type-based Polymorphic dispatch

(fizz-buzz proto-buzz [1 3 5 15 16])
(fizz-buzz proto-buzz [1.0 3.0 5.0 15.0 15.9])

WITHOUT breaking Equality or existing semantics

(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”.

decomplect information (nondestructive fizzbuzz)

All fizz-buzzes except PeanoBuzz lose information This is Very Very Bad. Can’t undo entropy.

Composite Data Buzz

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
  )

Clojure Spec’d Buzz

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
 )

Wicked pprint Buzz

@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!

What more could we possibly decomplect?

Well, *Transducers*.

(whatever, input -> whatever) -> (whatever, input -> whatever)

Seems like a good project for the bar, later on.

  • Rich Hickey

What are we decomplecting?

  • 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.)

Setup: basic-buzz we already had

(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))

Demo 1: decomplect Computation and Output format

(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!

Demo 2: decomplect Computation and Input format

(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.

Demo 3: Use only the decomplected xform as a calculator

(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.

Acknowledgments

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

Fin

https://www.evalapply.org/posts/n-ways-to-fizzbuzz-in-clojure/

fizzbuzz@evalapply.org

λ

_\//

Buzz

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment