Published on

Some Clojure Basics

Authors

As discussed in a previous post I setup Clojure on my machine with the aim of using it to solve the problems in the 2018 Advent of Code challenge. My aim of doing this is to get a feel for the Clojure language.

It is quite a shift from other languages I have worked with. It is certainly more terse but with that comes a steeper learning curve. I finished the day one challenge. The code for this can be seen below:

(require '[clojure.string :as str])

(def lines (str/split (slurp "./resources/day1.txt") #"\n"))

(defn as-signed-long [strnum]
  (let [[sign & rest] strnum]
    (def num (read-string (str/join rest)))
    (if (= sign \-) (* -1 num)  num)))

(defn day1 []
  (reduce + (map as-signed-long lines)))

(print (day1))

Based on my attempt and eventual solution to the problem I learnt the following things about the language.

Creating a Project

This is done using lein new app your-project-name

In the above app is a lein template. Templates are modules that can be installed using lein plugin install plugin-name. A list of plugins can be found here.

All Functions Return A Value In Clojure

You do not and cannot specify a return value. It seems (based on my initial experiences with the language) that whatever is on the last line is returned:

(defn say-hello [name]
  (str "Hello " name "!"))

(say-hello "John")
> Hello John!

If the function runs a command that does not return something then nil is returned:

(defn func-that-printlns [foo]
  (println foo))

(def res (func-that-printlns "Blah"))

(type res)
> nil

(print res)
> nilnil

Working with Characters

Clojure does have the concept of characters i.e. individual letters or numbers. But despite it being a JVM language you refer to these characters using different syntax. For example if you want to refer to the single character a, in most languages you would do so as 'a' in Clojure you instead use a back slash: \a. For example to check if a character variable foo is equal to the - character:

(= foo \-)
> true

String Destructuring

  • This is done using let together with apply where apply is used to work on the destructured parts.
  • You can return the result of the let parameter as the last result in your function. For example in the advent of code 2018 day 1 challenge to convert a string number with a sign e.g. "-122321" to a signed number:
(defn as-parts [strnum]
  (let [[sign & rest] strnum]
    (def num (read-string (str/join rest)))
    (if (= sign \-) (* -1 num)  num)))

Determining The Data Type of Variables

I found the easiest way to work on a solution to a problem was to play around with the problem in the lein repl. In doing so it is not always clear what the types are of the variables you are working with. To this end you can use the type command:

(type some-variable)
> java.lang.String

This is very useful in determining if you need to transform the given variable or/and what operations are available on it.

Loading Files from the Classpath

Coming from a JVM background came in handy here when trying to grok how to read files in a Clojure file. You refer to the root of the classpath using ./.

For example if I have the following directory structure:

projectRoot
-> src
   -> your
       -> package
           -> someFile.clj
-> resources
    -> some-package
        -> someprops.txt

You would load someFile.clj in the lein repl as:

(load-file "./src/your/package/someFile.clj")

The above assumes you are running the repl from inside a project directory anywhere in the project.

You can also run the file using cat someFile.clj | lein repl but this is much slower as it has to startup the repl first.

To read a file in a .clj for example if you are processing a text file you can use the slurp command which I found to be the most concise way to do this. For example say I want to load a file someprops.txt I would do this as:

(slurp "./resources/some-package/someprops.txt")

Various Tips and Gotchas

Tips

  • Code Formatter: Having some sort of code formatter in your editor of choice helps a fortune in working out if there are brackets missing or some too many.
  • Comments: are as simple as (comment stuff-you-want-to-comment)

Gotchas

  • Joining Sets: You cannot conj sets together you have to use clojure.set/union
(def set1 #{1 2 3})

(def set2 (clojure.set/union set1 #{1 3 4 5 6}))

(print set2)
> #{1 4 6 3 2 5}
  • Iterating Through Lists Concisely: Iterating through a list is a bit more tricky (at least to someone new to clojure) than it would seem as there are a number of different ways to do this. The most succinct way I found to do this (in my brief search) was:
(def some-list [1 3 5 6])

(doseq [num some-list] (print num))