Functional—organize DSLs around functions |
You organize your entire DSL as a collection of functions.
In Clojure, functions are first-class artifacts with strong support for higher-order functions and closures. Anonymous functions
are also supported.
In spite of the fact that Clojure is built upon Java, the language is predominantly functional. You can go down to the Java
level and invoke object semantics, but idiomatic Clojure is functional.
|
(str "hello" " " "world") => "hello world" (count [1 2 3 4 5]) => 5 (+ 12 20) => 32
Prefix notation is the normal order of the day.
Note:
Clojure is homoiconic. Note that every function invocation is a list that begins with the function name.
(filter even? [1 2 3 4]) => (2 4)
even? is a Clojure function that returns true if the input is an even number. In the previous example, we pass the function
even? as a parameter to filter, which applies even? to every element of the passed sequence.
(filter #(or (zero? (mod % 3)) (zero? (mod % 5))) [1 3 5 7 9 10 15]) => (3 5 9 10 15)
filter can also take an anonymous function.
|
Functional—function definition |
You define a function using defn. That’s pure syntax, which I illustrate in the example. The most interesting part is that
in Clojure (like any other Lisp variant), a function is also data that starts with a symbol.
|
(defn ^String greet "Greet your friend" [name] (str "hello, " name))
defn starts the beginning of a function definition. The next part is the documentation associated with the function definition,
called the docstring. Then we have the parameter list in a vector, and finally the body of the function. Optionally, you can have metadata with
the ^ prefix. Here it indicates the return type of the function.
The whole function definition is also a Clojure list that contains members for every part of the definition.
|
Designing abstractions |
The Clojure way |
Traditional OO way |
- Public fields
- Immutable objects
- Polymorphism through multimethods and protocols
- No implementation inheritance
|
- All data is hidden inside classes through private members
- Objects are mutable
- Polymorphism through inheritance hierarchies, which can mean inheritance of interface or implementation
- Implementation inheritance allowed
|
Sequences |
Every aggregate data type in Clojure is a sequence. You can treat sequences uniformly through a set of APIs that apply equally
to every member of the sequence family. You can also treat all Java collections as Clojure sequences. Take a look at the examples.
|
(first '(10, 20, 30)) => 10 (rest [10, 20, 30]) => (20, 30) (first {:fname "rich" :lname "hickey"}) => [:fname "rich"]
In the code snippet:
- The first example invokes first on a List.
- The second example invokes rest on a Vector.
- The third example invokes first on a Map.
|
Sequences are functions |
Clojure treats every sequence type as a function. This follows from the mathematical definitions of the sequences. Here’s
a list of the ways we can express Clojure’s sequences mathematically:
- A Vector is a function of its position.
- A Map is a function of its key.
- A Set is a function of membership.
|
(def colors [:red :blue :green]) (colors 0) => :red (def room {:len 100 :wd 50 :ht 10}) (room :len) => 100 (def names #{"rich hickey" "martin odersky" "james strachan"}) (names "rich hickey") => "rich hickey" (names "dennis Ritchie") => nil
|
Creating sequences |
Clojure offers a number of functions that you can use to create sequences. Many of them offer a lazy sequence as a result
and can be used to generate infinite sequences.
|
(range 0 10 2) => (0 2 4 6 8) (repeat 5 3) => (3 3 3 3 3) (take 10 (iterate inc 1)) => (1 2 3 4 5 6 7 8 9 10)
|
Filtering sequences |
Clojure offers combinators that you can use to filter a sequence. Always prefer using these combinators instead of coding
an explicit recursion.
|
(filter even? [1 2 3 4 5 6 7 8 9]) => [2 4 6 8] (take 10 (filter even? (iterate inc 1))) => [2 4 6 8 10 12 14 16 18 20] (split-at 5 (range 10)) => [(0 1 2 3 4) (5 6 7 8 9)]
|
Transforming sequences |
Clojure offers lots of combinators that transform an existing sequence. They take as input one sequence and generate another
sequence or value by applying some transformation.
|
(map inc [1 2 3 4]) => (2 3 4 5) (reduce + [1 2 3 4]) => 10
|
Persistent data structures and immutability |
In Clojure, all data structures are immutable and persistent. This means that after you make changes to an object, you can
access all historical versions of the same object without incurring additional overhead in storage requirements.
|
(def a [1 2 3 4]) (def b (conj a 5)) a => [1 2 3 4] b => [1 2 3 4 5]
|
Macros |
The secret sauce of DSL design in Clojure is macros. Macros are artifacts that get expanded to valid Clojure forms during
the macro-expansion phase. Macros are a very potent tool to use to design custom syntax constructs in your DSL.
|
(defmacro unless [expr form] (list 'if expr nil form))
The macro defines a control structure like if or while that you can use just like normal Clojure forms.
|