This is a story of Clara who attended a ClojureBridge workshop recently.
At the workshop, she learned what Clojure is and how to write Clojure code.
That really impressed her, "How functional!"
Also, Clara met an interesting drawing tool, Quil, which is written in Clojure.
When Clara learned how to use Quil, she thought, "I want to create something cool with Quil!"
Here's how Clara developed her own Quil application.
Clara considered where to start while looking at her first drawing application. Then, a small light turned on in her mind, "a white snowflake on a blue background looks nice." What she wanted to know was how to make a background blue and put a snowflake on it. Clara already learned how to find the way. It was:
- Go to the API documentation website
- Google it
Following that, Clara went to the Quil API web site, http://quil.info/api, and found the Loading and Displaying section. Then, she found the background-image function.
Then, she googled and found a StackOverflow question, Load/display image in clojure with quil, which looked like helpful. She also went to ClojureBridge drawing resources section, Quil and Processing Resources. Browsing at web sites listed there for a while, she found an interesting Xmas tree example, xmas-tree.clj, which displayed a xmas tree read from a file in a window. "This should be what I want," she delighted in getting those useful search results.
OK, for now, she got enough information to accomplish step 1.
At the ClojureBridge workshop, Clara went though the Quil app
tutorial,
Making Your First Program with Quil,
so she already had the drawing
Clojure project. She decided to use
the same project for her own app.
Clara added a new file under src/drawing
directory with the name
practice.clj
. At this point, her directory structure looks like the
below:
drawing
├── LICENSE
├── README.md
├── project.clj
└── src
└── drawing
├── core.clj
├── lines.clj
└── practice.clj <-- this file is added in step 1-1
First, Clojure source code has a namespace declaration, so Clara
copy-pasted ns
from the top of her lines.clj
file. But, she
changed the name from drawing.lines
to drawing.practice
, because
her new file has the name practice
.
At this moment, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]))
Basic Quil code has setup
and draw
functions, along with a
defsketch
macro, which defines the app. Following these Quil rules,
Clara added those things into her practice.clj
.
Now, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]))
;; setup and draw functions and q/defsketch are added in step 1-3
(defn setup [])
(defn draw [])
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:draw draw
:features [:keep-on-top])
Looking at the Quil API and the StackOverflow question, Clara learned
that where to put her image files was important. She created a new
directory, images
, under the top drawing
directory, and
put two images there.
Now, her directory structure looks like the one below:
drawing
├── LICENSE
├── README.md
├── images
│ ├── blue_background.png <-- added in step 1-4
│ └── white_flake.png <-- added in step 1-4
├── project.clj
└── src
└── drawing
├── core.clj
├── lines.clj
└── practice.clj
So far, the images were ready, the next step was to code using the Quil API.
Clara was back to the xmas tree example and learned her application needed
one more library to add, [quil.middleware :as m]
within :require
.
This was a quite new experience to her. To figure out what's that and how
to use, Clara walked through the document
Functional mode (fun mode).
When she finished the document, she murmured, "Ha, fun mode, nice name, isn't it? What I should do here is... to add":
[quil.middleware :as m]
in thens
form:middleware [m/fun-mode]
in theq/defsketch
form- function argument
state
todraw
function
"to my practice.clj
. OK, let's do it!"
At this moment, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m])) ;; this line is added in step 1-5
(defn setup [])
(defn draw [state]) ;; argument state is added in step 1-5
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode]) ;; this line is added in step 1-5
Clara already knew the final pieces to load images were:
- in
setup
function, load images and return those as a state - in
draw
function, look the contents instate
argument and draw images
So, she added a few lines of code to the setup
and draw
functions in
her practice.clj
. She was careful when writing the image filenames
because it should reflect the actual directory structure.
At this moment, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(defn setup []
;; these two lines, a map (data structure) is added in step 1-6
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")}
)
(defn draw [state]
;; q/background-image and q/image functions are added in step 1-6
(q/background-image (:background state))
(q/image (:flake state) 200 10)
)
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode])
When Clara ran this code, she saw this image:
Woohoo! She made it!
Clojure has a nice feature called,
destructuring.
Using the destructuring in a function argument, we can write draw
function like this:
(defn draw [{flake :flake background :background}]
(q/background-image background)
(q/image flake 200 10))
Clojurians often use this handy feature.
Clara was satisfied with the image of the white snowflake on the blue background. However, that was boring. For the next step, she wanted to move the snowflake like it was falling down. This needed further Quil API study and googling.
As far as she searched, moving some pieces in the image is called animation. The basic idea is:
- draw the image at some position
- update the position
- draw the image at updated position
So-called animations repeat these steps again and again.
"Well," Clara thought, "What does 'moving the snowflake like it was falling down' mean in terms of programming?"
To draw the snowflake, she used Quil's image
function, described in
the API:
image.
The x and y parameters were 200 and 10 from the upper-left corner, which was the position she set to draw the snowflake. To make it fall down, the y parameter should be increased as time goes by.
In terms of programming, 'moving the snowflake like it was falling down' means:
- Set the initial state--for example,
(x, y) = (200, 10)
- Draw the background first, then the snowflake
- Update the state - increase the
y
parameter--for example,(x, y) = (200, 11)
- Draw the background again first, then the snowflake
- Repeat 2 and 3, increasing the
y
parameter.
In her application, "changing state" includes only the y
parameter. How could she increment the y
value by one?
Yes, Clojure has the inc
function. This is the function she used.
To update y
parameter:
-
Add an initial
y
parameter in the map ofsetup
function, which represents state{:flake (q/load-image "images/white_flake.png") :background (q/load-image "images/blue_background.png") :y-param 10}
-
Add a new function
update
which will increment they
parameter by one(defn update [state] ;; updating y paraemter by one (update-in state [:y-param] inc))
-
Add the
update
function in theq/defsketch
form(q/defsketch practice :title "Clara's Quil practice" :size [500 500] :setup setup :update update :draw draw :features [:keep-on-top] :middleware [m/fun-mode])
So far, the app got the feature to update y
parameter;
however, this is not enough to make the snowflake falling down.
The snowflakes should be put on the updated position.
Clara changed the draw
function so that q/image
could have updated
y
parameter.
(defn draw [state]
;; drawing blue background and a snowflake on it
(q/background-image (:background state))
(q/image (:flake state) 200 (:y-param state)))
She added one more function, (q/smooth)
, to setup
since this would
make animation move smoothly.
At this point, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(defn setup []
(q/smooth) ;; added in step 2-2
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:y-param 10} ;; added in step 2-1
)
;; update function is added in step 2-1
(defn update [state]
(update-in state [:y-param] inc))
(defn draw [state]
(q/background-image (:background state))
(q/image (:flake state) 200 (:y-param state)) ;; changed in step 2-2
)
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:update update ;; added in step 2-1
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode])
When Clara ran this code--hey! She saw the snowflake was falling down.
Clara got a nice Quil app. But, once the snowflake went down beyond the bottom line, that was it. Only a blue background remained on the window. So, she wanted it to repeat again and again.
In other words: if the snowflake reaches the bottom, it should come back to the top. Then, it should fall down again.
In terms of programming, what does that mean?
If the y
parameter is greater than the height of the image, y
parameter should go back to 0
. Otherwise, the y
parameter should
be incremented by one. That said, there exist two cases to update y
parameter.
Clara recalled if
was used for
Flow Control
at the ClojureBridge workshop. It looked if
would handle the two cases
well like this:
(defn update [state]
(if (>= (:y-param state) (q/height)) ;; y-param is greater than or equal to image height?
(assoc state :y-param 0) ;; true - get it back to the 0 (top)
(update-in state [:y-param] inc) ;; false - update y paraemter by one
))
So, she used if
to make the snowflake go back to the top in the
update function.
At this point, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(defn setup []
(q/smooth)
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:y-param 10})
(defn update [state]
;; these three lines were added in step 3
(if (>= (:y-param state) (q/height))
(assoc state :y-param 0)
(update-in state [:y-param] inc)))
(defn draw [state]
(q/background-image (:background state))
(q/image (:flake state) 200 (:y-param state)))
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:update update
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode])
Clara saw the snowflake appeared from the top after it went down below the bottom line.
Clara thought, "It's nice to look at the snowflake falls down many times. But I have only one snowflake. Can I add more?" She wanted to see more snowflakes falling down: one or more on the left half, as well as one or more on the right half.
Again, she needed to express her thoughts by the words of programming
world. Looking at her code already written, she figures out, "This
would be 'draw multiple images with the different x
parameters.'"
The easiest way would be to copy-paste (q/image (:flake state) 200 (:y-param state)
multiple times with the different x
parameters. For example,
(q/image (:flake state) 10 (:y-param state))
(q/image (:flake state) 200 (:y-param state))
(q/image (:flake state) 390 (:y-param state))
But, for Clara, this did not look nice; she learned a lot about Clojure and wanted to use what she knew.
First, she thought about how to keep multiple x
parameters. She
remembered there was a Vectors
in the
Data Structures
section, which looked a good fit in this case.
Here's what she did to add more snowflakes:
-
Add a vector which has multiple
x
parameters assigned to a name withdef
.(def x-params [10 200 390]) ;; x parameters for three snowflakes
-
Draw snowflakes as many times as the number of
x
-params usingdoseq
.(doseq [x x-params] (q/image (:flake state) x (:y-param state)))
- See, doseq
At this point, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def x-params [10 200 390]) ;; added in step 4
(defn setup []
(q/smooth)
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:y-param 10})
(defn update [state]
(if (>= (:y-param state) (q/height))
(assoc state :y-param 0)
(update-in state [:y-param] inc)))
(defn draw [state]
(q/background-image (:background state))
(doseq [x x-params] ;; two lines were changed
(q/image (:flake state) x (:y-param state)))) ;; in step 4
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:update update
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode])
"Yes!" Clara shouted when she saw three snowflakes kept falling down.
Although her app looked lovely, Clara felt something was not quite right. In her window, all three snowflakes fell down at the same speed like a robots' march. It did not look natural. So, she wanted to make them fall down at different speed.
Using programming terms, the problem here is that all three snowflakes
share the same y
parameter.
Given that using multiple y
parameters would solve the problem--but how?
As she used vector
for the x parameters, the vector
would be a good data
structure to have different y
parameters as well. However, this
should not be a simple vector since each snowflake will have two
parameters, height and speed. Having these two, Clara could change the
falling speed of each snowflake.
Well, she was back to ClojureBridge curriculum and went to
Data Structures.
There was a data structure called Maps
which allowed her to save
multiple parameters. Looking at maps examples, she changed the y-param
from a
single value to a vector of 3 maps. Also, the keyword was changed from
y-param
to y-params
. Now her initial state became this:
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:y-params [{:y 10 :speed 1} {:y 150 :speed 4} {:y 50 :speed 2}]}
It was a nice data structure, actually maps in a vector in a map
including outer map. Downside was, her update
function would not be
simple anymore. What she had to do was updating all y
values in
the three maps within a vector.
Thinking both map and vector at the same time was confusing to her, so
she decided to think about map only. Each map has y
parameter and
speed
:
{:y 150 :speed 4}
as an initial state. The very next moment, speed should be added to y value. As a result, the map should be updated to:
{:y 154 :speed 4}
To accomplish this map update, Clara added update-y
function:
(defn update-y
[m]
(let [y (:y m)
speed (:speed m)]
(if (>= y (q/height)) ;; y is greater than or equal to image height?
(assoc m :y 0) ;; true - get it back to the 0 (top)
(update-in m [:y] + speed)))) ;; false - add y value and speed
Using Clojure's destructuring, we can write update-y
function like
this:
(defn update-y
[{y :y speed :speed :as m}]
(if (>= y (q/height))
(assoc m :y 0)
(update-in m [:y] + speed)))
As in the code, we can skip let binding.
Next step is to update maps in the vector using update-y
function.
Before writing this part, Clara cut down the problem to focus on
updating a vector: how to update contents in a vector. She remembered
there was a map
function which allowed her to apply a function to
each element in the vector.
map
function
For example:
(map inc [1 2 3]) ;=> (2 3 4)
Clojure has a
map
function andmap
data structure. Be careful, this is confusing. In Python, function is amap
, data structure is dictionary. In Ruby, function is amap
orcollect
, data structure is hash.
She tested the function on the insta-REPL:
(defn update-test
[m]
(let [y (:y m)
speed (:speed m)]
(if (>= y 500)
(assoc m :y 0)
(update-in m [:y] + speed))))
(def y-params [{:y 10 :speed 1} {:y 150 :speed 4} {:y 50 :speed 2}])
(map update-test y-params)
;=> ({:y 11, :speed 1} {:y 154, :speed 4} {:y 52, :speed 2})
It looked good, so she got back to her practice.clj
file to change
update
function. This function should return the state as the
map which includes :y-params
key with the update vector as a value.
Her update
function became like this:
(defn update [state]
(let [y-params (:y-params state)]
(assoc state :y-params (map update-y y-params))))
Another challenge was the draw
function change to see values in the
maps which were in the vector.
Clara found a couple of ways to repeat something in Clojure. Among
them, she chose dotimes
and nth
to repeatedly draw images; the
nth
function is the one in the curriculum:
Data Structures
In this case, she knew there were exactly 3 snowflakes, so she changed the code to draw 3 snowflakes as shown below:
(let [y-params (:y-params state)]
(dotimes [n 3]
(q/image (:flake state) (nth x-params n) (:y (nth y-params n)))))
At this point, her entire practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def x-params [10 200 390])
(defn setup []
(q/smooth)
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:y-params [{:y 10 :speed 1} {:y 150 :speed 4} {:y 50 :speed 2}]}) ;; changed in step 5-1
;; update-y function was added in step 5-2
(defn update-y
[m]
(let [y (:y m)
speed (:speed m)]
(if (>= y (q/height))
(assoc m :y 0)
(update-in m [:y] + speed))))
(defn update [state]
(let [y-params (:y-params state)] ;; update function
(assoc state :y-params (map update-y y-params)))) ;; was changed in step 5-3
(defn draw [state]
(q/background-image (:background state))
(let [y-params (:y-params state)] ;; three lines below were changed in step 5-4
(dotimes [n 3]
(q/image (:flake state) (nth x-params n) (:y (nth y-params n))))))
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:update update
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode])
When she ran the code, three snowflakes kept falling down at different speeds. It looked more natural.
Clara looked at her practice.clj
thinking her code got longer for a while.
Scanning her code from top to bottom again, she thought
"x-params
may be part of the state," for example:
[{:x 10 :y 10 :speed 1} {:x 200 :y 150 :speed 4} {:x 390 :y 50 :speed 2}]
She found that this new data structure was easy to maintain the state of each snowflake.
To use this new data structure, she changed her setup
function to
return the initial state which included x parameters also. The key
name was changed from :y-params
to :params
:
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:params [{:x 10 :y 10 :speed 1}
{:x 200 :y 150 :speed 4}
{:x 390 :y 50 :speed 2}]}
Clara stared at update-y
function and concluded to leave as it was.
Since existence of :x
and its value didn't affect updating y value.
The function returned the map with three key-value pairs with updated y value.
What about update
function? This needed a little change since key
name was changed from :y-params
to :params
.
(defn update [state]
(let [params (:params state)]
(assoc state :params (map update-y params))))
The draw
function would have a bigger change since the way to
extract x values was changed.
At first, Clara changed dotimes
function like this:
(let [params (:params state)]
(dotimes [n 3]
(q/image (:flake state) (:x (nth params n)) (:y (nth params n)))))
But, the exact the same thing, (nth params n)
, appeared twice.
"Is there anything better to avoid repetition?" she thought.
The answer was easy - use let
binding within dotimes
function.
Using let
, her dotimes
form turned to:
(let [params (:params state)]
(dotimes [n 3]
(let [param (nth params n)]
(q/image (:flake state) (:x param) (:y param)))))
The last line got much cleaner!
At this moment, her entire practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(defn setup []
(q/smooth)
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:params [{:x 10 :y 10 :speed 1} ;; changed in step 6
{:x 200 :y 150 :speed 4} ;;
{:x 390 :y 50 :speed 2}]}) ;;
(defn update-y
[m]
(let [y (:y m)
speed (:speed m)]
(if (>= y (q/height))
(assoc m :y 0)
(update-in m [:y] + speed))))
(defn update [state]
(let [params (:params state)] ;; changed to params in step 6
(assoc state :params (map update-y params)))) ;;
(defn draw [state]
(q/background-image (:background state))
(let [params (:params state)] ;; changed in step 6
(dotimes [n 3] ;;
(let [param (nth params n)] ;;
(q/image (:flake state) (:x param) (:y param)))))) ;;
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:update update
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode])
She saw the exact same result as the step 5, but her code looked nicer. This sort of work is often called "refactoring".
Clara was getting much familiar with Clojure coding. Her Quil app was getting much more fantastic, as well!
It was amusing to look at snowflakes falling down in different speeds. But, something she didn't like was... all of the snowflakes were falling straight down. This was much better than marching robots: however, it would be awesome if snowflakes were swinging left and right as falling down.
In the words of programming, the x
parameter should either increase or
decrease when the value is updated. This means that the update
function should update the x
parameters as well as the y
parameters so that snowflakes took a trace like this:
This would be a big challenge to her.
Clara already knew from her experience on y value: different swing parameters to three x values would give her nice swing motion. She changed the initial state to have swing parameter like this:
[{:x 10 :swing 1 :y 10 :speed 1}
{:x 200 :swing 3 :y 100 :speed 4}
{:x 390 :swing 2 :y 50 :speed 2}]
The swing
parameters should work to give different ranges between
leftmost and rightmost of the curve.
Her setup
function became like this:
(defn setup []
(q/smooth)
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:params [{:x 10 :swing 1 :y 10 :speed 1}
{:x 200 :swing 3 :y 100 :speed 4}
{:x 390 :swing 2 :y 50 :speed 2}]})
Updating x values were quite similar to the one for y values.
Like Clara added the update-y
function, she was about to write
update-x
function. But she stopped and thought, "How can I calculate
updated x values?" sketching a curve in her mind.
She went to Quil api document and scanned the functions thinking some
might have helped her. She found
sin
function which
was to calculate the sine of an angle. Also she recalled what was the shape
of sine curve.
The curve she want had roughly the shape of:
x = sin(y)
If she considered the size of window, a couple more parameters were needed to make swing look nice, for example:
x = x + a * sin(y / b)
The parameter a
exactly works as the swing
she added to the state.
When the swing
is 1, the curve traces like in the left image. While
the swing
is 3, the curve becomes the right image.
The parameter b
adjusts distances between peeks. If the value is
small, the snowflake goes left and right busily. On the other hand, if
the value is big, the snowflakes moves loosely. Thinking of the size
of window, 50 would be a good number for b
.
Given that, the update function would be:
x = x + swing * sin(y / 50)
Not just update x values, the function should handle the cases x is
smaller than 0 (too left), and x is greater than image width (too
right). When the x value goes to less than 0
, it should take the value of the
image width, so that the snowflake will appear from the right.
Likewise, when the x value goes to more than the image width, it
should have value 0
so that the snowflake will appear from the left.
Reflecting this conditions, her update-x
function became
like this:
(defn update-x
[m]
(let [x (:x m)
swing (:swing m)
y (:y m)]
(cond
(< x 0) (assoc m :x (q/width)) ;; too left
(< x (q/width)) (update-in m [:x] + (* swing (q/sin (/ y 50)))) ;; within frame
:else (assoc m :x 0)))) ;; too right
In this function, she couldn't use if
anymore since if
takes only one
predicate (comparison). Instead of if
, she used cond
which allowed
her to handle multiple comparisons.
Clara got update-y
and update-x
functions. The next step would be
to use these functions to update x and y values in maps in the vector.
This would not be a big deal for her anymore. Tricky thing was it
needed after updating y values in maps, it should update x values in
the same maps. Luckily, Clojure's let
binding treats this sort of
value changes well. Within let
, each binding is evaluated from the
first to last one by one. The update
function turned to this:
(defn update [state]
(let [params (:params state)
params (map update-y params)
params (map update-x params)]
(assoc state :params params)))
The first binding assigns params vector to a name, params
. The second
binding updates y values in the params
(maps in vector) and assigns to the
name, params
. The third binding updates x values in the params
(maps in vector) and assigns to the name, params
. When params
comes to the body of let
function, in other words, the line of
assoc
, both x and y values are already updated.
These were all to swing the snowflakes. She could use draw
function
as it was.
At this point, her entire practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(defn setup []
(q/smooth)
{:flake (q/load-image "images/white_flake.png")
:background (q/load-image "images/blue_background.png")
:params [{:x 10 :swing 1 :y 10 :speed 1} ;; swing was added in step 7-1
{:x 200 :swing 3 :y 100 :speed 4} ;;
{:x 390 :swing 2 :y 50 :speed 2}]}) ;;
;; this update-x function was added in step 7-2
(defn update-x
[m]
(let [x (:x m)
swing (:swing m)
y (:y m)]
(cond
(< x 0) (assoc m :x (q/width))
(< x (q/width)) (update-in m [:x] + (* swing (q/sin (/ y 50))))
:else (assoc m :x 0))))
(defn update-y
[m]
(let [y (:y m)
speed (:speed m)]
(if (>= y (q/height))
(assoc m :y 0)
(update-in m [:y] + speed))))
(defn update [state]
(let [params (:params state)
params (map update-y params)
params (map update-x params)] ;; added in step 7-3
(assoc state :params params)))
(defn draw [state]
(q/background-image (:background state))
(let [params (:params state)]
(dotimes [n 3]
(let [param (nth params n)]
(q/image (:flake state) (:x param) (:y param))))))
(q/defsketch practice
:title "Clara's Quil practice"
:size [500 500]
:setup setup
:update update
:draw draw
:features [:keep-on-top]
:middleware [m/fun-mode])
When Clara ran this code, she saw snowflakes were falling down, swinging left and right tracing sine curve. "Cool!" she shouted with joy.
Still, she could a couple more improvements including refactoring, Clara was satisfied with her app. Moreover, she started thinking about her next, more advanced app in Clojure!
The End.
Snowflake is designed by Freepik, http://www.flaticon.com/packs/snowflakes