Friday, December 1, 2017

Clojure for Those Who don't want to Learn Clojure

I really like Clojure. As a general purpose language I have not found a language I like more.

But when a team I worked on told me that they were migrating our code to another language we had an influx of team members who had never read Clojure and had no real incentive to learn it.

I will be the first to admit that Clojure is different. I struggled to read it at first, and I'll bet that others who have never dealt with a LISP will struggle too. I hope that my insignificant drop in the massive sea of helpful Clojure literature might help you.

This guide is not intended to be a complete guide to Clojure, but it might help you read a few things you maybe hadn't seen before.

Before I go repeating what is already out there, take a look at these resources:
Honestly, if you read these resources, you probably never need to look at this post ... Imma gonna write it anyway.

Why is Clojure Different?

Before I begin, I feel like I need to explain my take on why Clojure is great. I'm not writing this to convince you that you should use Clojure, but so that you can understand the philosophy behind Clojure and help understand some of the decisions made regarding the language or code you may confront.

Simplicity

Clojure is a LISP. What that means is there is almost no syntax because virtually everything is a list. The fancy term for this is homoiconicity which means that Clojure code is data. A list starts and ends with parenthesis. () is an empty list.

Every function is a list and functions are invoked using lists. The first item in a list is usually a symbol to a function that is invoked. fn is a function that creates functions. defn is also function which creates functions. If you've ever looked at even the smallest part of Clojure code, you've probably seen these before.

Consider, conversely, how complex other syntax can be. In many languages there are special meanings for

  • ;
  • :
  • ,
  • //
  • \
  • (
  • )
  • {
  • }
  • |
  • ?
  • =
  • +
  • -
  • *
  • /
  • %
  • for
  • while
  • if
  • etc.

or some combination of the above and/or other symbols, and there are special rules on how to use them. With Clojure you invoke functions with lists, semicolons are for comments, and you have a relatively small number of special symbols. You may feel I'm being pedantic by adding in reserved keywords like for and while, but I included them, because in LISPs those are just functions and they are handled like any other function.

Clojure does have a little more syntax than more traditional LISPs because some additional syntax can be helpful for things like maps, vectors, and sets. Following are examples of the literal syntax followed by what happens when that syntax is read by the reader.

;map literal
{:key :value ...}     <== this
(map :key :value ...} <== transforms to this

;vector literal
[0 1 2 3]
(vector 0 1 2 3)

;set literal
#{1 2 :bugga}
(set [1 2 :bugga])

Again, I may be accused of being pedantic, but I will say this, there are a lot fewer symbols with special meanings in Clojure, and when you see them they always mean the same thing. I will admit, that when you start working with macros you have more special symbols, but for most of your work in Clojure you'll only confront the symbols I mentioned already.

Power

Understanding that everything is a list if vital to understanding just how simple LISPs can be. Because the syntax of Clojure is so simple, it is more (not less!) powerful than many other languages. It literally allows you to extend functionality of the language. If there is a language feature you can't find in Clojure's core, you can write it. This is done using macros. Macros are beyond the scope of this post, but what they do is take a list (the parameters you pass in) and reorganize the contents of the list to something the reader already understands. For example. I mentioned that defn was a function, which is true, but only because macros are functions. defn is a macro that rewrites the contents it is passed.

(defn hello-world [name]
  (str "Hello, " name))

Is really a shortcut for

(def hello-world
  (fn [name]
    (str "Hello, " name)))

If your language doesn't have a foreach, but you want one, you probably can't add it, but you can extend the language with Clojure.

REPL

The REPL is a sandbox environment that makes it easy to test your code without going over a lot of hurdles. A lot of other languages are starting to use a command line style REPL (though they all call it something different) so you may already be familiar with the concept. When working with code that you don't understand, run it in the REPL to see what it does.

Functions

By default you cannot mutate bindings, once a binding is created it will always be that value. There is one way to get around this (that I sometimes find useful, but might make other Clojure developers cringe) and that is with the let function.

;let bindings allow you to shadow variables
(let [x 1
      x 3]
  x) <== returns 3

But it is important to know that each binding for x is a different reference underneath. Immutability is very useful especially when dealing with concurrency.

I know that my first question when I was told that you couldn't mutate variables was, "... well... how do you do anything then?"

The answer is quite simple—functions.

Functions can contain an arbitrary number of parameters. (+ 1 2 3 4 5 ...) returns 1 + 2 + 3 + 4 + 5 + ....

Functions can be called, passed, or created anywhere in your code. One way to do a loop may be to use recursion:

;recursion
(defn sum-list
  ([l] (sum-list l 0))
  ([l s] (if (empty? l)
           s
           (sum-list (rest l) (+ (first l) (or s 0))))))

This creates a multi-arity function (multiple variable signatures are allowed) if you call (sum-list [1 2 3]) the code will call sum-list again with the list passed and 0 as the sum. It will then call sum-list with all of the items except the first item, and with the sum of the first item in the list and the sum passed forward (0). It will keep doing this until there is no more list and then it will return the final sum.

There are two things I'll say about this. First, I invoked sum-list inside the two arity section, but I really should have used recur, I did this for clarity. Second, I don't think I've written any code that used recursion that is currently being used in a production environment. I've used map, reduce, and other functions that iterate over a list of items, but I don't know if I've ever needed to use recursion. With that in mind, I'll show a couple ways to write this

(defn sum-list [l]
  (reduce (fn [sum item] <== creates a function that takes the current sum and the current item in the list
             (+ sum item))
          l))

(defn sum-list [l]
  (reduce + l)) <== since + is a variable arity function, this works too

(defn sum-list [l]
  (apply + l))

These are all equivalent ways to do a sum in addition to the + function already shown.

Tips and Tricks

Now that I've had a chance to explain a little about why Clojure is so powerful and why someone might want to use it, I want to take a moment to write a few things you may not see in other languages.

Default Values Wrapped in (or)

In many languages, when you want to have a default value for a variable you must wrap this in an if block.

#pseudocode for default
function sum(a, b){
  if(a == null){
    a = 0;
  }
  return a + b;
}

If you're familiar with JavaScript you'll notice that a common way to deal with this is to use an or.

#same functionality in javascript
function sum(a, b){
  a = a || 0;
  return a + b;
}

- or -

function sum(a, b){
  return (a || 0) + b;
}

This works because JavaScript uses truthy values. You can do the same thing with Clojure.

; same functionality in Clojure
(defn sum [a b]
  (+ (or a 0) b)

Threading macros.

There are two threading macros, thread-first and thread-last. These functions are an awesome way to skip most of the parenthesis and it makes your code easier to read. For example, if you had the map:
{:a {:b {:c {:d {:e "blarg" :something-else :stuff}}}}}

and you wanted the value found in [a b c d e] you have multiple options. You could use get-in like this:

(get-in x [:a :b :c :d :e])

or this

(:e (:d (:c (:b (:a x)))))

But you could also do this:

(-> x :a :b :c :d :e)


which would extend it to this:

(:e (:d (:c (:b (:a x)))))


The threading macro threads the return of one call into the first (or last) parameter of the next function in line. This is useful when you have several transformations you want to do on the same thing. For example. Let's say you wanted to multiply x by 10, divide the returned value by 17, then add 2 and get the modulus of 100:

(-> x
  (* 10)
  (/ 17)
  (+ 2)
  (mod 100))

may easier to read than

(mod (+ (/ (* x 10) 17) 2) 100)

Clojure Spec

Clojure Spec is honestly a revelation! It allows the flexibility of a dynamically typed language to leverage the power of a type system. It does this so well, in fact, that Clojure Spec is more powerful than the type systems of most languages I've seen. I've already listed it, but I would visit https://clojure.org/guides/spec to read more.

Anonymous Functions and Partial

Anonymous functions allow you to create a new function from an existing function.

#(str % %2 %3)

is shortcut for

(fn [a b c] (str a b c))

You simply take the parameters that are passed in, in numbered order % or %1, %2, %3, ... %n and you pass them to the function you're calling. You cannot nest more than one anonymous function, but even one is helpful.

Partial is also a shortcut for creating new functions.

(def add-3 (partial + 3))

creates a function add-3 that when called will add 3 to the value(s) passed to it.

No comments:

Post a Comment

Have a question, comment, suggestion, or interesting bit of information you'd like to share? Please feel free to speak your mind.

(Blog comments that are spam, crude, have vulgar language (even mildly vulgar), are offensive, or otherwise inappropriate will summarily be deleted.)