Skip to content

Instantly share code, notes, and snippets.

@lostintangent
Last active February 28, 2025 14:45
Show Gist options
  • Save lostintangent/0602462eb5c874a794568c6f8d50a537 to your computer and use it in GitHub Desktop.
Save lostintangent/0602462eb5c874a794568c6f8d50a537 to your computer and use it in GitHub Desktop.
ClojureFam [Archived]

ClojureFam

In order to learn Clojure, and start participating in the Athens Research community, I signed up for the ClojureFam program. I'm part of Team Phanes and will be using this journal to document my learning journey over the coming weeks πŸ™Œ

Valuable references:

πŸ“† Day 1 (August 3, 2020)

Summary: I read the first 2 chapters of Clojure from the Ground up, and then completed the first 10 problems in 4Clojure.

Clojure is a dialect of Lisp, and has a grammar that is composed entirely of lists (Lisp == "List processing"). Lists are simply S-expressions, that start with a verb (a function), and then specify zero or more arguments (e.g. (inc 4), (+ 5 5)).

Lists can be "nested", which allows you to create complex expressions, that are ultimately composed into entire applications (e.g. (inc (inc 4))). Lists are evaluated left-to-right, but innermost lists evaluate before outermost lists.

In Clojure, most types evaluate to themselves (e.g. numbers, boolean, maps, keywords), however, there are two that evaluate to something else: lists and symbols.

All languages express the text of their programs in different ways, but internally all construct a tree of expressions. Lisp simply makes it explicit.

Java Interop

Clojure is built on top of the JVM (it's a "guest language"), and as a result, is able to interop with the entire/existing ecosystem of JVM classes and Java libraries. However, Clojure has its own dev tool (Leiningen), to simplify development (e.g. scaffolding, building, REPL, etc.).

Expression Quoting

By default, all lists in Clojure are evaluated, in order to actually execute the program. However, if you want to treat an expression as "data", then you can quote it by prefixing it with a ' (e.g. '(inc 5)).

Expression Verbs

While functions may be the primary "verb" in a Clojure list/expression, the language also supports using other types in the verb-position.

Type Behavior Example
Function Evaluate the function (inc 3)
Keyword Access a member of a map (:foo mapObject)
Set Determine if the set contains a member (setObject :key)
List/Vector Access a member of the vector by index (listObject 3)

Like all languages, Clojure's type system includes support for numeric types (integers, decimals), strings, booleans, and has a base library of functions for operating on them (e.g. (and true true true), (> 3 2 1)).

Furthermore, Clojure includes support for composite/collections data structures, such as lists (tuples), sets, vectors (arrays), and maps (associative arrays). Each of these types have both a functional construction form (e.g. (list 1 2 3)), as well as a syntactic/literal form.

Type Literal Syntax
List '(1 2 3)
Vector [1 2 3]
Set #{ 1 2 3}
Map {:name "mittens" :weight 9}

Common Functions

In order to access and mutate collection types, you can use any of the built-in functions that are provided by the Clojure standard library. The following are some of the most common ones, which apply to all collection types:

  • conj - Append an element to a collection
  • first - Retrieve the first element of a collection
  • second - Retrieve the second element of a collection
  • nth - Retrieve the nth element of a collection
  • count - Get the number of memebers in the collection

πŸ“† Day 2 (August 4, 2020)

Summary: I read chapter 3 of Clojure from the Ground up, and then completed problems 11-15 in 4Clojure.

Let expressions allow you to bind immutable values to symbols, that are visible the context of an expressions (e.g. (let [+ -] (+ 2 3))). You can specify n-number of let bindings as part of a let expression, and they are evaulated from left-to-right.

Functions

Functions allow you to define expressions, that reference dynamically provided bindings (e.g. (fn [x] (+ x 1))). Functions can be defined using a "normal" form, that explicitly defined named parameters, or a "shorthand" form that references parameters based on their position.

Form Example
Normal (fn [x] (+ x 1))
Shorthand #(+ % 1)

Vars

Vars allow you to define symbols that are globally accessible and mutable (e.g. (def cats 5)). Aside from defining arbitrary symbols, vars allow you to give functions a name:

Form Example
def (def half (fn [number] (/ number 2)))
defn (defn half [number] (/ number 2))

Documentation

In order to describe the behavior of a function, you can specify a "doc string" as part of its definition:

(defn launch
  "Launches a spacecraft into the given orbit by initiating a
   controlled on-axis burn. Does not automatically stage, but
   does vector thrust, if the craft supports it."
  [craft target-orbit]
  "OK, we don't know how to control spacecraft yet.")

You can then easily retrieve the documentation a function by running the following command: (doc launch).

Introspection

In order to retrieve arbitrary information about a function, you can use the built-in meta (e.g. (meta launch)). Additionally, you can retrieve the actual source code of a function using the source function (e.g. (source launch))

πŸ“† Day 3 (August 5, 2020)

Summary: I read chapter 4 of Clojure from the Ground up, and then completed problems 16-25 in 4Clojure.

As a functional programming language, Clojure makes heavy use of recursion as opposed to relying on iteration statements (e.g. for, while). Furthermore, as with most languages, it includes a built-in map function that allows you to apply a "transform" function across every member of a collection (e.g. (map inc [1 2 3 4])).

Sequences

While you can define collections statically (e.g. (list 1 2 3)), Clojure also supports "sequences", which are "lazy" collections, that are evaluated on-demand when a consumer actually asks for more members. This is useful for capturing potentially large (or infinite) collections, without needing to pay the cost of it (compute or memory), unless/until it's actually needed.

Creating Sequences

Function Description Example
iterate Creates a sequence by calling a provided function (iterate inc 0)
repeat Repeats a value n-number of times (repeat 3 :foo)
range Create a sequence based on a range of numbers (range 2 10)
cycle Creates a sequence by "cycling" through the values of a specific collection (cycle [1 2 3])

Consuming Sequences

Function Descrpition Example
take Retrieves n-number of values from a sequence (take 5 sequence)

πŸ“† Day 4 (August 6, 2020)

Summary: I read chapter 5 of Clojure from the Ground up.

Macros allow you to define custom functions, which re-write code before it's actually evaluated. As a result, "macro expansion" represents a stage in Clojure's evaluation model, that occurs before expressions are evaluated. A macro is defined using the defmacro function, which looks almost identical to the defn function.

Since Clojure's syntax is equivalent to it's internal AST ("code as data"), writing macros is easier than in other languages, since you're not operating on text (as in C) or some new/unfamiliar data structure (as in other languages like JavaScript). You're literally writing functions that operate on lists, which is already what you're used to do in Clojure for writing applications. However, the key difference with macros is in what the function returns: code.

Syntax Quoting

Since macro expressions return code and not values, they allow you to return "templates" strings, that is made up of a Clojure expression, with some values interpolated into it. You define a "syntax quoted" expression by prefixing it with a tilde.

Within the template string, you can use the following special "operators" to interpolate and/or expand the meaning of symbols:

| Syntax | Description | |-|-|-| | ~ | Replace the value of a symbol | | ~@ | Replace the "spreaded" value of a symbol (e.g. if the symbol points at a list then return 1 2 3 not (1 2 3)) | | # | When appended to a symbol name, it will generate a unique version of that symbol name, so that it doesn't conflict with other symbols in the outer expression |

πŸ“† Day 5 (August 7, 2020)

Summary: I read chapter 6 of Clojure from the Ground up.

As with all functional languages, Clojure references are immutable by default, and therefore can't be changed. However, Clojure allows you to create mutable references using the following types: vars, atoms, and refs.

Note: In order to refer to a var, atom or ref, you need a symbol. And therefore, these types don't "replace" symbols. However, they provide an extra layer of indirection between the symbol and it's "underlying" value.

Furthermore, Clojure is built to enable concurrent programming, and therefore, provides some powerful primitives to delay, parallelize and synchronize computation: delay, future and promise. The following table explains the differences between these types of references and concurrency primitives:

Type Mutability Reads Updates Evaluation Scope
Symbol Immutable Transparent Lexical
Var Mutable Transparent Unrestricted Global/Dynamic
Delay Mutable Blocking Once only Lazy
Future Mutable Blocking Once only Parallel
Promise Mutable Blocking Once only
Atom Mutable Nonblocking Linearizable
Ref Mutable Nonblocking Serializable

Legend:

  • Transparent - Value is returned simply by referencing the symbol, as opposed to requiring the use of deref or @.
  • Blocking - Attempting to deref will cause the current thread to block until the referenced state is available (e.g. the promise has been delivered)
  • Lazy - Evaluation doesn't occur until a consumer attempts to dereference it.
  • Parallel - Evaluation happens immediately (as opposed to lazily), but occurs on a separate thread.

πŸ“† Day 6 (August 8, 2020)

Summary: I read chapter 7 of Clojure from the Ground up.

Clojure projects are composed of a project.clj file, which defines its build properties: name, description, version and dependencies. Furthermore, it includes a src directory for source code, a test directory for tests, and a docs directory for documentation.

Namespaces

Within *.clj file, you can define a new namespace, which allows you to group a collection of related symbols together. Furthermore, a namespace expression can also define a set of other namespaces that it depends on, and therefore, can access symbols from. This allows namespaces to decompose a project into smaller pieces/files, while retaining reusability.

(ns scratch.core)

; Import another namespace
(ns user (:require [scratch.core]))

; Import and alias another namespace
(ns user (:require [scratch.core :as c]))

; Import a specific symbol from another namespace
(ns user (:require [scratch.core :refer [foo]]))

Project Dependencies

In order to add a dependency to external/3rd-party packages, you simply add a reference to the project.clj file, that specifies the name and version of the package you want to use. Behind the scenes, packages are pulled from Clojars, which is the package manager community for Clojure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment