Wow, I've taken quite the long break from working through Seven Languages in Seven Weeks. But I am still determined to go through it (and thankfully its a long weekend this weekend). Not only that, but I discovered I had done half the exercise for day 2 over 3 months ago! So picking up the pieces took a little while. I was racking my brain on why stopped midway and working though the chapter again gave me a clue. But I'll get back to that later. The main topics for day 2 of Clojure are sequences, macros and protocols and records.
Clojure has an abstraction over many of its collections known as the sequence. Many of the typical operations over containers are defined over the sequence abstraction. This includes common functions such as map, filter and reduce, as well as a few predicates such as every?, some? and not-any?. Another aspect of Clojure we are introduced to is lazy evaluation, which in the context of sequences allows us to create and efficiently work with infinite sequences, taking just what we need since they are lazily generated.
(take 3 (repeat "developers"))
In the snippet above, (repeat "developers"), generates an infinite sequence of the string "developers". (take N S) is a functions that gets N values from the sequence S, in this case giving us the sequence ("developers" "developers" "developers"). There are a number of functions that build and manipulate infinite sequences, and in the book there is an elegant implementation of fibonnaci using infinite lazy sequences.
Macros seem to be a defining aspect of lisps, they allow the programmer to manipulate code (at compile/read time — not runtime), and generate new code, allowing one to add new forms to the language. The uniform expression of everything in Lisp as a list make macros appear quite natural. Macros generate new lists that can then be executed as code.
The need for macros as a special form to achieve this list generation can be described by remembering what we saw in Io day 2. In particular how Io passed its arguments around as unevaluated messages, this made it really easy to define new control structures such as unless as simple methods. Like most languages Clojure evaluates arguments before passing them into functions, thus we cannot just use a normal method to implement something like unless as for example both the true and false condition would execute before the test just to call the method. That last statement may not be strictly true, there may be an 'eval' in Clojure to which we could pass a list that represents our code in order to get it to run it. But this is exactly what macros are for, and they remove the need for the user of the new form (such as unless) to pass in the code in a form that won't be automatically evaluated.
This is how an 'unless' form can be defined in Clojure using macros (exercise 1).
; & is used to define optional params (defmacro unless [test truebody &[falsebody]] (list 'if test falsebody truebody)) ; this is another way to support optional params (defmacro unless ([test truebody] (list 'if (list 'not test) truebody)) ([test truebody falsebody] (list 'if test falsebody truebody))) ;usage (unless (> 1 3) (println "t1: not greater than")) (unless (> 1 3) (println "t2: not greater than") (println "t2: greater than")) (unless (> 5 3) (println "t3: not greater than") (println "t3: greater than"))
When the program is read by the interpreter, it expands the macro and effectively replaces references to unless with the list of code generated by the macro (substituting in the arguments as necessary). You can see in the generation of the final form we want the macro to evaluate to (unless truebody falsebody -> if test falsebody else truebody || if not test truebody) that we need to quote things we don't want to evaluate in the body of the macro itself like 'if and 'not. You can also see that we just return regular lists, they just happen to contain code. Well hopefully I haven't massacred the concept, but it is a powerful one that provides the programmer more ways to adapt the language to their particular use case.
Protocols and Records
In re-reading the section on protocols and records it dawned on me why I likely stopped halfway through the exercises. The section on these two aspects of Clojure was, in my opinion, somewhat lacking. And the exercise doesn't really help shed much light on how these features fit into the language. At the time the book was being written, protocols and records were just being added to the language, so the description of how they fit in is somewhat thin. Now, the general concept of what they are/do does comes through, but i still am not sure how they fit into the typical ways to model a program in clojure. But first what are they.
Protocols provide a way to specify an interface that datatypes can conform to. They mirror the concept of an interface in java and have method declarations but no implementations. Records (and other clojure datatypes) are types that implement these interfaces, and thus allow for method dispatch based on type (polymorphism similar to that in Object Oriented Programming). Here is an example (and my solution to exercise 2, "Write a type using defrecord that implements a protocol").
(defprotocol Performer "Performers come in all forms" (perform [this] "Perform your talent") (demand [this] "Make a demand") (act-out [this] "Act out in dramatic fashion") ) (defrecord RealityStar  Performer (perform [_] (println "I already am!")) (demand [_] (println "I demand you keep me on this show")) (act-out [_] (println "Gonna hoard all the tp in this house!")) ) (defrecord RockStar  Performer (perform [_] (println "Party like a rock—Party like a rockstar!")) (demand [_] (println "Bring me an empanada from that old Columbian lady in Staten Island. You know who i mean")) (act-out [_] (println "I'm shutting down the studio!")) ) ;create values of the two record types (def tyree (RealityStar. )) (def tron (RockStar. )) ;call functions with the values. The correct implementation will be selected based on type (demand tyree) (demand tron) (act-out tyree) (act-out tron)
The code above describes a Performer interface that various kinds of performer can implement. And the correct implementations of the function are chosen based on the type of value the function is called with. Overall its very familiar to OOP and I can see its utility because I am used to OOP. That results in part of my insecurity about how they should be used in a functional language like clojure (not that type based dispatch is inherently OOP, so maybe that is the lesson here).
I know they are used to provide some abstraction over sequences in implementing clojure itself (so that you can have functions the process vectors from sets differently for example), so it could be I am looking for there to be something more here than there actually is. I also imagine that they *might* be useful for interacting with java objects and types (just a wild guess on my part, haven't seen any of the mechanics of how that is done in clojure). Overall the chapter didn't seem to provide much context for how they are likely to be used in a typical clojure program. Though that might be asking a lot for a quick language tour; I don't really expect to come away with a lot of deep understanding of how to model things in clojure through this. So at this point I guess I am just describing what had me turn to look for other sources and more info about this feature (during which I guess I got distracted/busy, hence the long break from this blog :).