Clojure for Domain-specific Languages - Design Concepts with Clojure

Exclusive offer: get 50% off this eBook here
Clojure for Domain-specific Languages

Clojure for Domain-specific Languages — Save 50%

Learn how to use Clojure language with examples and develop domain-specific languages on the go with this book and ebook

$32.99    $16.50
by Ryan D. Kelker | December 2013 | Open Source

In this article, by Ryan D. Kelker, author of the book Clojure for Domain-specific Languages, we will go over some basic concepts that apply to software development in any programming language. Each section will explain what the concept is and why the concept should be applied to your projects. As with all sources of information, choose whichever works for you.

This article will cover:

  • Pure functions
  • General programming concepts
  • Clojure writing styles

By the end of this article, you should be able to articulate the concepts of DRY, KISS, YAGNI, and bottom-up development. In addition to this, you should have a better understanding of how to write nicer Clojure as the anti-patterns section displays many examples of both what to do, and what not to do.

(For more resources related to this topic, see here.)

Every function is a little program

When I first started getting deep into Clojure development, my friend Tom Marble taught me a very good lesson with a single sentence. I'm not sure if he's the originator of this idea, but he told me to think of writing functions as though "every function is a small program". I'm not really sure what I thought about functions before I heard this, but it all made sense the very moment he told me this.

Why write a function as if it were its own program? Because both a function and a program are created to handle a specific set of problems, and this method of thinking allows us to break down our problems into a simpler group of problems. Each set of problems might only need a very limited collection of functions to solve them, so to make a function that fits only a single problem isn't really any different from writing a small program to get the very same result. Some might even call this the Unix philosophy, in the sense that you're trying to build small, extendable, simple, and modular code.

A pure function

What are the benefits of a program-like function? There are many benefits to this approach of development, but the two clear advantages are that the debugging process can be simplified with the decoupling of task, and this approach can make our code more modular. This approach also allows us to better build pure functions. A pure function isn't dependent on any variable outside the function. Anything other than the arguments passed to the function can't be realized by a pure function. Because our program will cause side effects as a result of execution, not all of our functions can be truly pure. This doesn't mean we should forget about trying to develop program-like functions. Our code inherently becomes more modular because pure functions can survive on their own. This is key when needing to build flexible, extendable, and reusable code components.

Floor to roof development

It is also known as bottom-up development and is the concept of building basic low- level pieces of a program and then combining them to build the whole program. This approach leads to more reusable code that can be more easily tested because each part of the program acts as an individual building block and doesn't require a large portion of the program to be completed to run a test.

Each function only does one thing

When a function is written to perform a specific task, that function shouldn't do anything unrelated to the original problem it's needed to solve. For example, if you were to write a function named parse-xml, the function should be able to act as a program that can only parse XML data. If the example function does anything else other than parse lines of XML input, it is probably badly designed and will cause confusion when trying to debug errors in our programs. This practice will help us keep our functions to a more reasonable size and can also help simplify the debugging process.

Clojure for Domain-specific Languages Learn how to use Clojure language with examples and develop domain-specific languages on the go with this book and ebook
Published: December 2013
eBook Price: $32.99
Book Price: $54.99
See more
Select your format and quantity:

Patterns for success

If you're reading this article, you're probably already familiar with some of the programming principles that'll be covered in this article. Don't feel discouraged if you don't and please review these principles if you do. This section will cover three important programming principles that can be used together to write better code. These three principles are DRY, KISS, and YAGNI. As you'll see, each principle has an inherently minimalistic nature to its core concepts and works nicely when applied with others.

DRY

Don't repeat yourself: The concept of DRY is fairly simple. This programming concept has become one of the most fundamental concepts in modern programming. The idea is to produce cleaner code by writing abstractions that eliminate repetition.

Reasonably so; duplication causes unnecessary bloating and requires modifications to be applied in multiple locations for the same change. Because duplicate code is scattered across multiple locations, when a change is made in one location and not in others, bugs are often the consequence of not using the DRY principle. The bigger picture of the DRY principle isn't necessarily to eliminate all forms of duplication, but to eliminate multiple ways of expressing the same thing. You have to ask yourself, what's easier to modify and maintain? One representation of one thing or three completely separate representations of the same thing? Using the DRY principle helps to avoid building and maintaining multiple representations, and ultimately leads to easier maintenance and cleaner code.

KISS

Keep it simple, stupid: This is the principle of breaking things down into subtasks and keeping your solutions as clear and as simple as possible. Even though the complexity of software is increased when adding functionality, this doesn't mean that the method of implementation can't be simple. When using the KISS principle, you should try to solve the problem before you begin writing your solution. This will help you break up the implementation of your solution into smaller, simpler steps.

YAGNI

You aren't going to need it: This principle works very well with the KISS principle because you try to keep things minimal and simple by only adding functionality when needed. This helps to prevent unnecessary bloat and allows to focus more on the functionality that's needed immediately.

Writing Clojure

Since Clojure is a young language, the common anti-patterns of Clojure development are still evolving. If you're coming from another Lisp-based language, then you can probably transplant many of the best practices when developing with the Clojure language. It's very important, for those new to Lisp-based languages and Clojure in general, to review the evolving anti-patterns to write more maintainable and better code. With that said, style guides aren't law and should be broken when a more pragmatic approach can be taken.

This section will go over the proper way of writing Clojure code and then display the improper way of writing the same code. Sometimes showing what not to do is better than showing what to do, so this section tries to show both, with the hope that we can better understand the reasoning behind the method. Again, style guides aren't law, but they encourage best practices for real-world Clojure development.

Spacing and alignment

As with most programming languages, spacing and alignment are important in writing maintainable Clojure. Unlike Python, whitespace characters are ignored in most circumstances. This section will specifically look at how beautiful Clojure can be, and how ugly someone can make it.

When indenting in Clojure, you want to use two spaces rather than four spaces or tab characters, as shown in the following code snippet:

;; Yes (defn example [is-true? is-false?] (when (true? is-true?) (when (false? is-false?) (println "two space indent"))))

;; No (defn example [is-true? is-false?] (when (true? is-true?) (when (false? is-false?) (println "two space indent"))))

The exception to this rule applies when you're passing arguments to a function. There's no limit as to how many spaces can be used because the goal is to vertically align the arguments of the function call, as shown in the following code snippet:

;; Yes (example ["M.Fogus" "R.Hickey" "C.Emerick"] ["J.Rogan" "D.Stanhope" "B.Burr"])

;; No (example ["M.Fogus" "R.Hickey" "C.Emerick"] ["J.Rogan" "D.Stanhope" "B.Burr"])

This exception also applies to the let special form and Clojure collection types, as shown in the following code snippet:

;; Yes (let [x 1 y 2 z (+ x y)] (apply + [x y z]))

;; No (let [x 1 y 2 z (+ x y)] (apply + [x y z]))

;; Yes {:first "Brian" :middle "" :last "Redban"}

;; No {:first "Brian" :middle "" :last "Redban"}

;; Yes ["Apples" "Oranges" "Grapes"]

;; No ["Apples" "Oranges" "Grapes"]

;; Yes {"key" "value" "map" "literal"}

;; No {"key" "value" "map" "literal"}

;; Yes #{"hash-set" "tes-hsah" "odd count"}

;; No #{"hash-set" "tes-hsah" "odd count"}

Defining a function in Clojure is pretty clear, but there is a wrong way to do so, as shown in the following code snippet:

I ;; Yes (defn x [y] y)

;; Also okay (defn a [b] b)

;; No (defn l [m] m)

;; No (defn o [p] p)

;; Yes (defn x ([] (x 1)) ([y] (+ 1 y)))

;; No (defn y ([] (y 1)) ([z] (+ 1 z)))

Not enough whitespace leads to clutter and non-clarifying whitespace leads to longer lines of code, as shown in the following code snippet:

;; Yes (conj [1 2] 3)

;; No (conj[1,2]3)

;; Yes (do (x y))

;; No (do ( x y ) )

As commas aren't required, don't use commas when separating elements of Clojure's collection types, as shown in the following code snippet:

;; Yes [\a \b \c \d \e]

;; No [\a, \b, \c, \d, \e]

;; Yes {:os "archlinux" :machine "acer"}

;; Yes {:os "archlinux" :machine "acer"}

;; No {:os "archlinux", :machine "acer"}

If you're coming from a language such as ECMAScript/JavaScript, you might be tempted to close nested braces in the same way that you would do in JavaScript. You'll want to avoid this practice in Clojure because it can potentially cause confusion and your code will take more page room than necessary, as shown in the following code snippet:

;; Yes (defn xyz [i] (let [example? true] (fn [x] (if example? (* x i) (* 2 x)))))

;; No (defn xyz [i] (let [example? true] (fn [x] (if example? (* x i) (* 2 x) ) ) ) )

Organizing and grouping top-level Clojure forms make your code easier to read and manage, as shown in the following code snippet:

;; Yes (def _name "Ryan") (defn user [user-name] (str "User " user-name)) (def tool :can-opener) (def can {:can-opener "beans"}) (defn open-can [can] (tool can))

;; No (def _name "Ryan") (defn user [user-name] (str "User " user-name)) (def tool :can-opener) (defn open-can [can] (tool can)) (def can {:can-opener "beans"})

Syntax

Clojure's :pre and :post assertion test features are a great way for testing that the input and output of a function are indeed valid. Because of this wonderful feature, using :pre and :post should be preferred over performing checks that throw errors, as shown in the following code snippet:

;; Yes (defn example [x y] {:pre [(pos? x) (neg? y)] :post [(< 5 %)]} (+ x y))

;; No (defn example [x y] (let [value (+ x y)] (when (and (pos? x) (neg? y) (< 5 value)) value)))

Defining variables within a function can be a big mistake. You should write a macro to create a custom definition form, or use the let form to define local variables. In Clojure, all variables defined outside the let, if-let, and when-let forms are global in scope, as shown in the following code snippet:

;; Yes (defn example [& x] (let [str-version (apply str x) _ "Another local variable"] (-> str-version (clojure.string/reverse) first)))

;; No (defn example [& x] (def str-version (apply str x)) (-> str-version (clojure.string/reverse) first))

The names of function arguments need to be chosen wisely. If you're not careful, you'll clobber a globally-accessible function or variable. This may lead to unexpected results and will eventually lead to having to use the fully-qualified name of the variable or function you wish to use within your function, as shown in the following code snippet:

;; Yes (defn example [-map] (map #(let [[k v] %] (str "Key: " k " Value: " v)) -map))

;; No (defn example [map] (map #(let [[k v] %] (str "Key: " k " Value: " v)) map))

When testing a Boolean value, if the test is true, and there's nothing to be done when false, prefer when over the combination of if and do, as shown in the following code snippet:

;; Yes (when working? (a b c) (x y) (z))

;; No (if working? (do (a b c) (x y) (z)))

When needing to define and test a local variable, prefer the use of the if-let form over the combined use of let and if, as shown in the following code snippet:

;; Yes (if-let [apple (:apple fruits)] (println apple))

;; No (let [apple (:apple fruits)] (if apple (println apple)))

This should also be the case when needing to use the when form, as shown in the following code snippet:

;; Yes (when-let [apple (:apple fruits)] (println apple))

;; No (let [apple (:apple fruits)] (when apple (println apple)))

The use of if-not should be preferred over the combined use of if and not, as shown in the following code snippet:

;; Yes (if-not (xyz) (println "false"))

;; No (if (not (xyz)) (println "false"))

Prefer the use of the when-not form instead of the combined use of the if, not, and do forms, as shown in the following code snippet:

;; Yes (when-not xyz (println false))

;; No (if (not xyz) (do (println false)))

Using when-not should also be preferred over the combined use of the when and not forms, as shown in the following code snippet:

;; Yes (when-not xyz (println false))

;; No (when (not xyz) (println false))

Prefer the use of the not= form over the combined use of the not and = forms, as shown in the following code snippet:

;; Yes (true? (not= a b))

;; No (true? (not (= a b)))

The use of anonymous functions to make a single function call shouldn't be wrapped when calling forms such as flatten and map, as shown in the following code snippet:

(def nums [1 2 3]) ;; Yes (map inc nums)

;; No (map #(inc %) nums)

The use of complement should be preferred over the use of anonymous functions when needing the inverse of a function that returns a Boolean value, as shown in the following code snippet:

;; Yes (filter (complement nil?) [true false nil 123 nil])

;; No (filter #(not (nil? %)) [true false nil 123 nil])

To increase readability and prevent deep nesting, prefer the use of comp over the use of anonymous functions that wrap multiple function calls, as shown in the following code snippet:

;; Yes ((comp clojure.string/reverse str) 123456)

;; No (#(clojure.string/reverse (str %)) 123456)

As an alternative, use the thread last and thread first macros to increase readability and to prevent deep nesting of function parentheses, as shown in the following code snippet:

;; Yes (->> [1 2 3 4 5] (map str) first example)

;; No (example (first (map str [1 2 3 4 5])))

;; Yes (-> [1 2 3 4 5] reverse ((partial map str)) first)

;; No (first (map str (reverse [1 2 3 4 5])))

When calling multiple methods using Java interoperation, prefer single or double decimal interop notation over the use of thread-first macros, as shown in the following code snippet:

;; Okay (-> (java.util.UUID/randomUUID) .version)

;; Better (.version (java.util.UUID/randomUUID))

;; Best (.. java.util.UUID randomUUID version)

When testing a variable or an expression, prefer the case form over both the condp and cond forms as shown in the following code snippet:

;; Okay (cond (= x 5) :is-y (= x 4) :is-z :else :no-match)

;; Better (condp = x 5 :is-y 4 :is-z :no-match)

;; Best (case x 5 :is-y 4 :is-z :no-match)

Name conventions

Unlike many of the popular languages in use today, the use of underscores and camel case for either functions or variables names is frowned upon in Clojure, as shown in the following code snippet:

;; Yes (def clojure-global-variable 100)

;; No (def clojureGlobalVariable 100)

;; No (def clojure_global_variable 100)

;; Yes (defn clojure-global-fn [x] x)

;; No (defn clojureGlobalFn [x] x)

;; No (defn clojure_global_fn [x] x)

When writing a function that returns a Boolean value, add a question mark at the end of the function names to indicate that it returns either true or false, as shown in the following code snippet:

;; Yes (defn red? [x] (.. (str x) toLowerCase (contains "red")))

;; No (defn is-red [x] (.. (str x) toLowerCase (contains "red")))

;; No (defn red-p [x] (.. (str x) toLowerCase (contains "red")))

Dynamic definitions must attach the ^:dynamic metadata before the definition symbol, and the symbol must contain *stars* around the symbol to indicate that it is indeed dynamic and rebindable, as shown in the following code snippet:

;; Yes (def ^:dynamic *abc* 0)

;; No (def ^:dynamic abc 0)

When handling values that you aren't going to employ, use the underscore character for their assignment, as shown in the following code snippet:

;; Yes (def example ["red" "blue" "green"]) (defn use-example [[_ blue green]] (println blue green))

;; No (def example ["red" "blue" "green"]) (defn use-example [[red blue green]] (println blue green))

Collection types

When defining a key/value collection type, prefer the use of the Clojure keyword data type as a key over the use of string keys, as shown in the following code snippet:

;; Yes (hash-map :abc 123) (zipmap [:abc :def] [123 456]) {:abc 123}

;; No (hash-map "abc" 123) (zipmap ["abc" "def"] [123 456]) {"abc" 123}

When getting values from a key/value collection type, use the keyword data type to get the value from the collection, as shown in the following code snippet:

(def comedian {:name :Doug-Stanhope :like :Bill-Burr})

;; Yes (:name comedian)

;; Yes (get comedian :name :not-found)

;; No (comedian :name)

;; No (get comedian :name)

;; No (if-not (:name comedian) :not-found (:name comedian))

Summary

Every function is a little program. When writing a function, you should apply the Unix philosophy to create a more simple, flexible, and extensible function. The Unix philosophy, in terms of programming, is to build small, extendable, simple, and modular code. This will help simplify debugging and help prevent unwanted side effects that can be commonly attributed to a function written to perform more than one task.

Building an application from the bottom-up requires a strong set of low-level functions to better support the abstraction process when building higher-level functions. This approach encourages writing more modular and reusable code because each function acts as an individual building block. These building block functions will more likely be used in another project with little or no modifications. Limiting a function's capabilities to a single task helps prevent unwanted side effects and leads to creating better building blocks.

Resources for Article:


Further resources on this subject:


Clojure for Domain-specific Languages Learn how to use Clojure language with examples and develop domain-specific languages on the go with this book and ebook
Published: December 2013
eBook Price: $32.99
Book Price: $54.99
See more
Select your format and quantity:

About the Author :


Ryan D. Kelker

Ryan D. Kelker is a Clojure enthusiast and works as a freelance—he is willing to take on any project that sounds interesting. He started exploring computers and the Internet at a very early age and he eventually ended up building both machines and software. Starting with MS DOS, batch files, and QBasic, he eventually floated towards Arch Linux and the Clojure language.

He has four certifications from both CompTIA and Cisco, and has decided not to pursue any additional certifications. These days, he spend most of his time reading about software development, cyber security, and news surrounding up-and-coming computer languages. While away from the computer, he is usually reading a book or going out to eat with the people he loves the most.

Books From Packt


Mastering Clojure Data Analysis
Mastering Clojure Data Analysis

Clojure Data Analysis Cookbook
Clojure Data Analysis Cookbook

Clojure High Performance Programming
Clojure High Performance Programming

Groovy for Domain-Specific Languages
Groovy for Domain-Specific Languages

Implementing Domain-Specific Languages with Xtext and Xtend
Implementing Domain-Specific Languages with Xtext and Xtend

Groovy 2 Cookbook
Groovy 2 Cookbook

Instant HubSpot Dashboard Customization [Instant]
Instant HubSpot Dashboard Customization [Instant]

Object-Oriented JavaScript - Second Edition
Object-Oriented JavaScript - Second Edition


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software