Seven Languages In Seven Weeks: Clojure - Day 2

Day 2 of Clojure from Seven Languages in Seven Weeks was fairly challenging. I'd say the following code took me about 4 hours to write. Part of this was obviously due to novelty of the language; but, I believe that part of the challenge in today's assignment was the fact that Tate's explanations were quite different than those that I subsequently researched online. While I typically find the book's explanations a great starting point for research, I found today's segment more confusing than useful.

HW1: Implement an unless with an else condition using macros.

Before I get into my version of the homework, I just wanted to take a moment and explore the existing (unless) macro such that it may bring to light the necessary syntax for "code as data". In the book, Tate outlines the (unless) macro as such:

(defmacro unless [test body]

(list 'if (list 'not test) body)

)

What this code is doing, from what I can gather, is build up code based on the incoming parameters. In essence, it is building literal lists that will then be used as code on the subsequent pass (Clojure uses a preprocessor to run macros). This is part of the beauty of the "code as data" concept.

This works, but I found it to be very confusing to read; and, after a good deal of online reading, I found a much more simple way of executing the same concept:

(defmacro unless [test body]

`(if (not ~test) ~body)

)

This provides the same outcome as before, but is, in my opinion, a whole heck of a lot easier to read. The trick to this version is the use of the syntax-quote (` a.k.a. backquote) together with the unquote (~). While I can't fully explain what these elements are doing exactly, the syntax-quote appears to create a template for a data structure (ie. our lists) in which the contextual, unquoted values are replaced into the template.

In my mind, things are a bit more clear if I translate this concept over to the string interpolation functionality provided by ColdFusion:

"(if (not #test#) #body#)"

I believe that Clojure is actually doing more than simple substitution; but, for the time being, this simplistic view point is serving me well.

Ok, that said, let's take a look at the homework - creating an (unless) macro that has an "else" condition:

;-- Implement an unless with else condition using macros.

;-- Define the unless macro with an Else condition.

(defmacro

unless

[test if-body else-body]

`(if

(not ~test)

~if-body

~else-body

)

)

;-- --------------------------------------------------------- --

;-- --------------------------------------------------------- --

;-- Test the unless macro with a TRUE value. Because the unless

;-- condition tests for "not" test, calling it with true should

;-- result in the "else-body" executing.

(unless

true

(println "1: Hey, this is the IF condition!")

(println "1: Hey, this is the ELSE condition!")

)

;-- Test the unless macro with a FALSE value. Since we are calling

;-- it with a false, the "if-body" should execute.

(unless

false

(println "2: Hey, this is the IF condition!")

(println "2: Hey, this is the ELSE condition!")

)

Using the syntax-quote, this macro should be fairly easy to understand. This time, rather than just passing in a "body," we are passing in a IF body and an ELSE body; then, we are simply plugging these values into the (if) function. When we run the above code, we get the following console output:

1: Hey, this is the ELSE condition!2: Hey, this is the IF condition!

This works; but, I wanted to see if I could come up with a way to make the ELSE condition optional. The way the macro is coded now, if you tried to only use one body, the Clojure compiler would throw the following error:

The solution to this problem actually brings us to another piece of syntax related to the syntax-quote - the unquote-splicing (~@) operator. Unquote-splicing operator is like the unquote (~) operator in that is substitutes the contextual value of the given variable; however, the unquote-splicing doesn't do a literal substitution. Rather, it takes the value that we are substituting and merges its sequence into the target sequence such that the outcome is a flattened sequence.

I think this concept is best demonstrated with some pseudo-code. Image that we have a sequence:

(def myValue '(:a :b :c))

... and we try try to unquote (~) it into a syntax template:

`(foo bar ~myValue)

This would result in a direct substitution of the variable:

(foo bar '(:a :b :c))

If we use unquote-spicing (~@), on the other hand, the incoming sequence is actually merged into the target sequence. Doing this:

`(foo bar ~@myValue)

... would result in this:

(foo bar :a :b :c)

As you can see, unquote-splicing (~@) allows for a sort of flattened-substitution, where as unquote (~) is more of a literal substitution.

Ok, so going back to our problem, we want to provide an optional Else clause. Well, as it turns out, the (if) function already allows for an optional Else clause; and, since our macro uses the (if) function internally, providing optional arguments to the macro should allow us to provide optional arguments to the embedded (if) function:

;-- Implement an unless with else condition using macros.

;--

;-- NOTE: This time, however, we're going to account for a variable

;-- number of clauses (the ELSE is optional).

;-- Define the unless macro. This time, however, when we define the

;-- the "arguments", we're going to include the test (condition);

;-- but, we're going to enumerate the "rest" of the arguments in

;-- the parameter "bodies". This MAY contain one condition, it MAY

;-- contain two conditions.

;--

;-- Then, we creating the resultant code, we are using the ~@ (tilda

;-- ampersand) operator. This evaultes the bodies parameter as a

;-- sequence and appends it to the contextual sequence in which it

;-- was defined.

;--

;-- Basically, the ~@bodies operation may define the "true" and the

;-- "false" portions of the (if) function.

(defmacro

unless

[test & bodies]

`(if (not ~test) ~@bodies)

)

;-- --------------------------------------------------------- --

;-- --------------------------------------------------------- --

;-- Test with both true/false bodies. However, since we are

;-- passing in "true", our "else" body should execute.

(unless

true

(println "1: Hey, this is the IF condition!")

(println "1: Hey, this is the ELSE condition!")

)

;-- Test with just the "true" body and a "true" condition. This

;-- would execute the "else" body; however, since none is defined,

;-- this macro will not return anything.

(unless

true

(println "2: Hey, this is the IF condition!")

)

;-- Test with just a "true" body. And, since we are passing in

;-- "false", our "true" body should execute.

(unless

false

(println "3: Hey, this is the IF condition!")

)

This time, when defining our (unless) macro, I am telling it to expect one or more arguments. The one argument that we know about is the "test" argument. The ampersand (&) in the arguments list then allows us to define "everything else." That is, all the other arguments passed into the macro will be sucked up into the sequence defined as "bodies."

This might be one argument - the "true" body; or, it might be two arguments - the "true" and the "false" body. In either case, we are then unquote-splicing (~@) the "bodies" parameter sequence into the (if) function that we are invoking internally. Essentially, the unquote-splicing allows us to augment the "argument collection" used to invoke the (if) function.

When we run the above code, we get the following console output:

1: Hey, this is the ELSE condition!3: Hey, this is the IF condition!

As you can see, the "else" condition was optional.

HW2: Write a type using defrecord that implements a protocol.

Clojure doesn't have objects in the object oriented programming (OOP) sense; rather, it has data structures that have immutable values. Given an instance of a data structure, should you try to change a property of that data structure, what you'll get back is a completely new instance of that data type with the appropriate property configuration. The original instance remains unchanged.

Like object oriented programming, however, data structures do appear to support member functions. However, there doesn't appear to be an implicit binding of methods. You can associate a function implementation with a given data type; but, when you invoke that function, you have to explicitly pass in the data structure on which the function will be acting.

In the end, perhaps this provides for a form of "name-spacing" more than anything else - a way to provide different implementations of a named function for use within different contexts. I probably shouldn't try to explain this any further as I am sure I will start to mislead you (any myself).

For this assignment, I have created a Person protocol (think "interface" or "abstract" class) that defines three methods:

say-hello

talk

poo

Then, I am defining a Girl data type that implements the Person interface:

;-- Write a type using defrecord that implements a protocol.

;-- Define the protocol that we are going to implement. This is

;-- like an interface that a custom data type will "promise" to

;-- uphold.

;--

;-- When defining the methods, the first argument will always be

;-- a refernece to the data type on which the method is being

;-- invoked. Since we aren't really creating "objects" in the

;-- traditional sense, we need to pass a data structure to each

;-- function in the interface.

(defprotocol Person

;-- Say hello to the given person.

(say-hello [this name])

;-- Say something random.

(talk [this])

;-- Make poo.

(poo [this])

)

;-- --------------------------------------------------------- --

;-- --------------------------------------------------------- --

;-- Now that we have our "Person" interface, let's define a custom

;-- data type, Girl, that will promise to implement the Person

;-- protocol (and its methods).

(defrecord Girl

;-- Define constructor arguments. These will become properties

;-- of the data type instance.

[name]

;-- The list of protocols that we are implementing. In this

;-- case, we are only implementing the Person protocol.

Person

;-- Define the class methods. Here, we must define all of the

;-- methods listed in the protocols that this class will uphold.

;-- PERSON:: Say hello to the given person.

(say-hello

[this name]

(str "Hello " name ", my name is " (:name this))

)

;-- PERSON:: Say something random.

(talk

[this]

(rand-nth

'(

"Hello there."

"How are you?"

"Nice out today, isn't it?"

"I was just on my way to DSW."

"Can I have a mojito?"

)

)

)

;-- PERSON:: Make poo.

(poo

[this]

(str "Girls don't poo!!!")

)

)

;-- --------------------------------------------------------- --

;-- --------------------------------------------------------- --

;-- --------------------------------------------------------- --

;-- --------------------------------------------------------- --

;-- Create a new instance of the Girl data type.

(def joanna (Girl. "Joanna"))

;-- Now, call the methods in our data type. Notice that the first

;-- argument to the method is always the data type instance on which

;-- it is being called.

(println (say-hello joanna "Ben"))

(println (talk joanna))

(println (poo joanna))

As you can see, every function defined by the Person protocol takes "this" as the first argument. "this" is not the required name; I am simply using it as "this" is what we also use in ColdFusion and Javascript. Furthermore, you can see that Girl defines its own implementation of those methods; however, when invoked, the Girl instance on which we are acting must be passed-in during the invocation in order to create the association.

When we run the above code, we get the following console output:

Hello Ben, my name is JoannaI was just on my way to DSW.Girls don't poo!!!

Day 2 of Clojure was definitely mentally trying; but, it was fun to finally see some of these solutions come to light. On one hand, I get frustrated that I have to do so much online research; but on the other hand, that kind of research really drives the point home and commits the concepts to memory (or so I hope). I think day 3 is gonna get a bit more crazy. But, if I can make it through just one more day, I'll really feel like I was able to conquer a language that had previously conquered me!