Clojure for Rubyists
Clojure
Clojure code is made up of Clojure data structures. The primary one being a list. A list is a collection the elements of which can be accessed sequentially.
brew install lein
lein repl
The Clojure compiler will evaluate the code as is.
(+ 1 2 3 4)
The above is a Clojure data structure, a list, it is also valid source code. The first element of the list is a symbol (variable), followed by arguments. In this case the +
symbol is bound (assigned) to a function which accepts an unlimited number of arguments.
Because Clojure code is made up of lists, where the first element is expected to be a symbol it means that when we want to use a list of data we need to let the compiler not to evaluate it.
(+ 1 2)
is a list and it is evaluated as code. But what if you want an actual list in your programe, to signal to the compiler that you don’t want the list to be evaluated as part of the program use quote
.
(quote (1 2 3))
The above will yield a list, (1 2 3)
. Without quote
the compiler would try to evaluate 1
as a symbol.
(quote (1 2 3))
is a literal list, whereas (1 2 3)
is a list which is evaluated as a program, since 1
isn’t a symbol an error ocurrs.
A syntax sugar for quote is '
.
Lists
You can not access any element, only head and tail. Therefore access to elements is sequential, there is no random access.
You can thus access any element by getting the head or tail x number of times, but access is sequential.
(list 1 2 3)
(quote (1 2 3))
‘(1 2 3)
(first 1 2 3) ; 1
(last 1 2 3) ; 3
There is no equivalent in Ruby. The closest would be any Array for which random access using []
is not allowed.
A List behaviour is a subset of a Vector.
We can get any element, but to do this we have to iterate sequentially through the list. This will have performance issues for very large lists.
(nth (list 1 2 3 4 5 6) 5)
Vectors
Vectors are like list but with random access to the elements, not just sequential access.
(vector 1 2 3)
; [1 2 3]
[1 2 3]
; [1 2 3]
These are like an Array in Ruby, [1,2,3]
.
Vectors and Lists are usually interchangeable.
(get [1 2 3] 1] ; get the nth element
(conj [1 2 3] 4)
[1 2 3 4]
[1 2 3].push(4)
[1 2 3] << 4
The important difference is in Clojure, unlike Ruby, everything is immutable.
Using lists
(+ 1 2 3)
; 6
This is a list, the first element is an operator. The remaining elements numbers. In this example each number is an argument to the +
function, which can accept any number of arguments.
It looks like inject/reduce in Ruby, but it is not.
[1, 2, 3].reduce(:+)
# 6
This is more like:
(reduce + '(1 2 3))
; 6
In the above reduce
is given a function, +
, and a collection, the list, (1 2 3)
. The prefix '
prevents the list being evalutaed as Clojure source.
Anonymous functions
(fn [x] (* x x)
lambda { |x| x * x }
To use this function:
(def square (fn [x] (* x x)))
(square 10) ; 100
square = lambda { |x| x * x }
square.call(10) # 100
square[10] # 100
Reverse a collection:
(def reverse (fn [x] (reduce conj '() x)))
(reverse [1 2 3]) ; [3 2 1]
Explained:
conj
will append to a collection.
reduce
takes a function and a starting value and a collection.
Symbols (Varibles)
Variables are scoped to the current namespace and can be accessed anywhere within the namespace.
(def name “Kris”)
In the above name
is referred to as a symbol. And we bind the string “Kris”
to it.
name = “Kris”
Named functions
We can assign an anon function to a variable.
(def square (fn [x] (* x x))
(square 3)
; 9
The above is a list, the argument list is another list.
square = lambda { |x| x * x }
square.call(10)
square[10]
We can use some sugar, using defn, to make this a little more terse:
(defn square [x] (* x x))
(square 4) ; 16
defn
can also also accept a doc-string:
(def square “Squares the given number” x)
You can view the doc-string in the REPL: (doc square)
.
In Ruby defining a method is also simialr:
# squares the given number
def square(x)
x * x
end
square(10)
Defining local variables
(def my-function [x]
)
Other collections
Map
(hash-map :first_name ‘Kris’ :last_name ‘Leech’)
{:name ‘Kris’ :last_name ‘Leech’ }
(:first_name {:first_name ‘Kris’ :last_name ‘Leech’ })
(def person {:first_name ‘Kris’ :last_name ‘Leech’ })
(:first_name person)
:foo
is called a keyword in Clojure, like a symbol in Ruby, but not really…
A keyword is in fact a function which looks its self up in the given hash-map.
This is why we can do (:first_name person)
, :first_name is a function and the argument is person
. Mind blown.
You can also lookup a value using get
: (get { :first_name ‘Kris’ }).
Ruby: {:name “Kris” :last_name “Leech” }
person = {:name “Kris” :last_name “Leech” }
person[:first_name]
In Clojure you get nested values with get-in
:
(get-in { :person { :first_name ‘Kris’ } } [:person :first_name])
In Ruby: params[:person][:first_name]
Set
(set [1 2 3])
#{1 2 3}
(sorted-set [4 10 1 3]) ; #[1 3 4 10}
(get (set 1 2 3) 2) ; 2
(get (set 1 2 3) 100) ; nil
As with hash-map we can use a keyword (which is actually a function which looks itself up in the given collection): (:a (set [:a :b :c]))
Ruby:
Set.new([1,2,3]) [1,2,3].to_set
Namespaces
Like modules in Ruby
map / reduce
map is a higher order function, this means one of its arguments is another function.
(map inc [0 1 2 3])
Ruby:
inc = Proc.new { |n| n + 1} [0,1,2,3].map(&inc)
(map str ‘(1 2 3)) ; map fn collection [1, 2, 3].map(&:to_s) # collection map fn
select / reject
filter takes a function which must return true/false and a collection:
(filter #(> % 5) ‘(3 4 5 6 7))
Ruby: [3, 4, 5, 6, 7].select { |n| n > 5 }