How to learn Clojure effectively

When you come from a Ruby background, learning Clojure is a step into a foreign world. For starters, there is no concept of classes and methods and there is nothing you would recognize as variables. You also realize quickly why it's called a "functional" language — functions are everywhere, used all over the place, in different sizes and shapes.

As shocking as this contrast can be, for developers who are used to focussing on getting things done, it's not hard to ignore all these differences and look for equivalents of concepts they know. Just think of any toy project you could want to build as a learning exercise and think about the problem domain. Almost automatically, you come up with a mental model involving objects that contain data that can be manipulated using methods. It's like your brain is hardwired to object-oriented design. This is not a bad thing in itself, because brains are good at optimizing themselves for the patterns of thought that are used most often. But optimizations don't come without cost. In this case, it makes it harder for you to solve a problem top-down because the abstraction layer right below the problem domain is one that has no equivalent in the target programming language. It's like there is a forbidden zone somewhere between the problem and the syntax of the language that can't be bridged by learning about either of them.

Here are suggestions on how to fill this gap from the bottom up.

  1. Solve artificial, low-level problems instead of starting with a real-world use case.
  2. Read parts of the source code of the language that are written in the language itself.
  3. Learn about programming concepts that are independent of any programming language.

Artificial problems and reading code

I recommend using 4Clojure. It doesn't actually teach you the language, it doesn't explain how things are done in Clojure, it just presents you with a collection of puzzles. What's useful about these puzzles is that they are low-level enough to be in the safe zone where you don't automatically think in object-oriented patterns. They are also interesting in that the solution is usually not to just learn the syntax for how to do certain things. You have to actually do some research and explore the toolset the language provides.

Here is an example of the kind of problems 4Clojure provides (problem 19):

Let's look at this exercise and see what it wants to teach you. Getting the last element of a sequence can't be so hard, no? You just use the function last and continue with the next exercise, right? Well yes, only: not in this exercise. The most obvious solution is forbidden and you must look for other ways how to do it.

One idea would be to iterate over the list until the pointer is at the last element. That also happens to be the implementation of last:

Enter this implementation and press the "run" button on 4Clojure to see what your code golf score is (you need to be logged in and have code golf enabled):

Screen_shot_2011-09-08_at_9

Oh, your score is comparatively high which means that a lot of people have found shorter solutions than you. This usually is an indication that there is a language feature you don't know about. So let's search for this thing we don't know using the function find-doc:

Interesting — there is a function that returns the last element of vectors, but not of lists. For lists, it returns the first element. You ask yourself "Why is that?" and start reading about lists and vectors and what the differences are and when you would use one rather the other.

In the task, there are two vectors and one list, so it doesn't seem like too bad an idea to convert anything that comes in into a vector and then run peek on it to get at the last element. How can you create a vector from a list? Well, you search for a function that does that:

…and then you use it:

This was just one example of how solving simple artificial problems can teach you a lot about a new language.

Abstract concepts

With programming languages, it's like with any category of things where by knowing just one or two examples, you don't really have a feeling for what's optional and what's essential and what kind of differences there are that you could expect from one to the next. So they are like cities and natural languages and universities and … for whatever else this is true.

Structure and Interpretation of Computer Programs is a book and also a series of lectures freely accessible on the internet that is a classic in the realm of computer science, for a reason: they do a really good job at teaching you concepts of programming, independent of any specific programming language. They do use Scheme, which is a dialect of LISP for their code examples, but what they really do is make you think about programming as the process of creating a multi-layered, versatile language in which it is easy to solve the kind of problems your software is specialized on.

Here is one of my favorite quotes from the lectures:

What we are going to tell you in this course is that when you think about a language you think about it in terms of "what are the primitives", "what are the means of combination" (what are the things that allow you to build bigger things) and "what are the means of abstraction" (how do you take that bigger things that you've built and put black boxes around them and use them as elements to make something even more complicated).

Think about your favorite programming languages and try to answer these questions for them. Also think about how some things can not be combined very easily, for example methods in Ruby. You can combine method calls by wrapping them inside of a method, but you can't define a method inside of a method. You have to wrap methods inside of modules or classes in order to combine them. In Clojure though, you can very easily combine functions.

Think about data and how in Ruby you can use arrays and hashes to combine them and work with them as a more complex entity by iterating over items in the various ways Ruby provides, like map and inject or by building your own enumerators. These are powerful tools that allow you to model a lot of data structures and process them in a way that feels native to the language.

In Clojure, you find mind-blowing ways of solving problems very elegantly by combining functions in various ways. This is a far more interesting way of looking at different languages than just comparing the syntax or assigning them categories like "object-oriented" or "functional", because you will have an expectation of what to look for that is not based on the previous languages you've worked with, but on an understanding of the kinds of problems any language needs to solve in one way or another.