Part 1 one this series introduced Clojure programming beyond simple one-liners and expressions. We were setting up our environment and had a simple demonstration that illustrated the basics. We are using Quil, an implementation of Processing for Clojure, which is a great example of application of program flow that we want to transform into functional code. Our guideline is Matt Pearson’s book ‘Generative Art‘ with its inspirational graphics and code. So let’s get started and try to implement something more complex than we did last time.
This is what we want to draw:
I agree, this is not overly impressive. Not yet. But before we’re going into the more spectacular moves we have to learn the basic steps. To draw a growing circle you would write this Processing code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int diam = 10; float centX, centY; void setup() { size(500, 300); frameRate(24); smooth(); background(180); centX = width/2; centY = height/2; stroke(0); strokeWeight(5); fill(255, 50); } void draw() { if (diam <= 400) { background(180); ellipse(centX, centY, diam, diam); diam += 10; } } |
The basic principle Matt Pearson wants to show here is that you’re setting up things in a setup() function and that the drawing takes place in the draw() function. Draw() is an implicit loop: what’s happening inside draw() is executed again and again. While the various commands in setup() are rather straightforward, the interesting detail in draw() is the exit condition: no circle is drawn when the diam variable exceeds a value of 400.
When you have a look at the official Clojure version written by the authors of Quil you see a one-to-one implementation which even respects the use of the global variables centX, centY, and diam. This is implemented by using the Quil-only method set-state! which, in this case, corresponds to the aforementioned global variables. I wouldn’t recommend this technique in such a short example. Instead, I’ve come up with this Clojure code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
(ns for-the-glory-of-art (:require [quil.core :refer :all])) (defn setup [] (frame-rate 5) (smooth) (background 180) (stroke 0) (stroke-weight 5) (fill 255 50) (let [cent-x (/ (width) 2) cent-y (/ (height) 2)] (loop [diam 10] (when (<= diam 400) (background 180) (ellipse cent-x cent-y diam diam) (recur (+ diam 10)))))) (defsketch growing-circle :title "2.1" :size [500 300] :setup setup) (defn -main [&args]) |
First, we are omitting the draw() method at this time. This frees us from working with global variables or sketch state, because now everything is taking place in setup(). This is perfectly fine, because we are not wanting an endless loop at all: the whole drawing is finished after 40 steps, that’s the distance between 10 and 400 in steps of 10. So there really is no need for set-state!
The -main function and defsketch macro should be familiar to you from part 1 of this series: -main is the entry point, defsketch prepares the window frame. The first few lines in setup are almost like their Processing counterparts, only the variable assignments for cent-x and cent-y have moved into the following let block. Within this block we now have a loop that we are calling back using recur, incrementing the diam parameter by 10. The when condition tests for the value of diam; if it is smaller than 400, a circle of diameter diam is drawn.
This is today’s lesson’s insight: we could have been using a sketch-specific state using set-state! for simulating the global variables of the original Processing code, but we didn’t. We could have used the draw() function, but it wasn’t necessary in this peculiar case. Much more interesting is to understand what the original code actually does: execute drawing circles until a certain condition is met. Using Clojure’s loop and recur combined with a test condition checked with when exactly does that job.
The following parts of this series will further exercise this deconstruction of imperative code. The goal is not to slavishly imitate the original code but to find functional code that will produce the same result. Stay tuned.