Skip to content

Clojure

Kevin Kredit edited this page Apr 19, 2020 · 6 revisions

Notes and lessons from the Clojure programming language.

Language Features

Data

Data

  • Immutable by default

Types

  • Booleans
    • (boolean expr) evaluates to true or false
    • Only false and nil are falsey
  • Strings
    • Literals; concatenated with str, formatted (like sprintf) with format
    • Are actually vectors (at least, some seqable type) of characters
  • Integers
    • Basically everything is expected
    • bigint, or the N suffix for literals, is for very large numbers
    • Fractions represented as ratios 4/3 = 4/3, not 1
  • Lists
    • Collections of objects
    • Delared with '(1 2 3)
    • Objects do not need to be of the same type '(1 2 'a)
  • Vectors
    • More efficient than lists (Java vector class backed?)
    • Delared with [1 2 3]
    • Support Java interop, e.g. .indexOf [1 2 3] 3
    • Like lists, objects do not need to be of the same type [1 2 'a]
  • Sets
    • Collections of unique values
    • Order is not maintained
    • Delared with #{1 2 3}
    • Can do set operations such as conj, disj, sort, contains?, subset?, and superset?
    • Like lists and vectors, objects do not need to be of the same type #{1 2 'a}
  • Maps
    • Key-value data stores
    • Declared with {:Apple "Mac" :Microsoft "Windows"}
  • Sequences
    • Effectively the "supertype", or "typeclass" of all collections types (more accurately, all "seqable" types), including lists, vectors, sets, and maps.
    • Sequences are the type for which familiar functional operations are implemented, like map, reduce, iterate, take, take-while, doseq (like for/foreach), etc.
    • Sequence functions all return sequences
  • into: a function frequently used to convert between sequence types, e.g. (into [] '(1 2))

Variables

  • Declared with let
    • def creates global mutable variables, so is to be avoided
  • Shadowing is allowed
  • Uses lexical scope (variable is resolved to the one in the nearest scope)

Structure and Control

Execution and Control Flow

  • If
    • (if predicate br1 br2)
    • if-let binds the value of an expression to a variable if it's truthy
    • when is like if, but doesn't have the falsey branch and is wrapped in a do block, unlike if
    • when-let is the when version of if-let
  • Case
    • case uses pattern matching, BUT patterns must be compile time constants, and cannot be used for binding
    • cond is like case, but uses tests
    • Together, these are kind of like haskell's pattern matching and guards (but lamer)
  • For
    • for is not for loops; that is doseq
    • for is for list comprehensions! Similar to Haskell
    • Syntax:
      (for [x [0 1 2 3 4 5]
            :let [y (* x 3)]
            :when (even? y)] ; or "while" to stop when predicate is not met
         y)
      ; Result: (0 6 12)
  • Recursion
    • Supported; use recur to specify tail recursion and prevent stack overflows
    • loop is always used with recur, and is sued to specify the recursion point (by default, it is the function itself)
  • Macros
    • Allow definition of own syntax, control flow, and types (though you still won't get type checking)
    • ~@ "splices" a sequence into the enclosing syntax-quoted data structure. This occurs before evaluation of the enclosing structure, so can be used to manipulate code at runtime. Talk about code-as-data!
  • Threading macros
    • -> the thread-first macro: "It's first because it's passing down the evaluation of former forms to the first argument of preceding forms." It turns
      (c (b (a [] 1) 2) 3)
      into
      (-> []
        (a 1)
        (b 2)  ; implicit (b {result of prev step} 2)
        (c 3)) ; implicit (c {result of prev step} 3)
    • ->> the thread-last macro: "It's last because it's passing down the evaluation of former forms to the last argument of preceding forms." It turns
      (c 3 (b 2 (a 1 [])))
      into
      (->> []
        (a 1)
        (b 2)  ; implicit (b 2 {result of prev step})
        (c 3)) ; implicit (c 3 {result of prev step})

Functions

  • Defined with (defn name [args] (commands (of body)))
  • Evaluated with (name arg1 arg2)
  • Clojure is functional, and tries to be purer than other lisps
  • Have implicit dos, which are procedural
  • Defined unnamed with (fn [args] (commands (of body)))
  • Shortcuts
    • #() is a shortcut for fn, so you can do #(+ 1 1)
    • % is replaced with arguments; when multiple, %1, %2, etc
  • Curried by default
  • Supports closures
  • Must be defined above where they are used (is this the 1970s?)

Objects and Interfaces

  • Can use Java classes
  • seqable is essentially an interface for iterable items; it may have formal semantics and syntax, though Clojure By Example didn't get into it. It also may not, as Lisps are known for being low on syntax

Error Handling

  • Not much built into the language or type system, though I'm sure you can use functional patterns to do it elegantly (it just won't be forced at compile time :( )

Tooling

  • Leiningen is a project automation tool--package manage, build configurator, etc. It was easy to setup and start using.
  • The compilation errors are very poor Java stack traces. I hear the way to debug lisps is to evaluate forms directly. Hm.
  • Probably due to the nature of the language, there are no errors when you (accidentally) redefine a function. Can see this being tricky.
  • Notable VSCode extensions:
    • Clojure (avli.clojure)
    • Bracket Pair Colorizer 2 (coenraads.braket-pair-colorizer-2)

Feel

Very foreign, even from Haskell. The compiler is not your assistant. Instead, it seems, the way to debug is to evaluate forms directly. Good tooling is necessary for this style of development, and fortunately VSCode has good Clojure support, and the days are over when you had to use Emacs to efficiently develop a Lisp (though it's still probably the best).

I miss strong types. The presence of type-oriented runtime errors makes me uncomfortable. And the frustrating thing is that Clojure has types, because you get runtime errors like ClassCastException java.lang.Boolean cannot be cast to clojure.lang.IFn while debugging, you just can't check these types at compile-time. I know the Clojure development workflow is REPL-oriented instead of compile-oriented, but even when you create working code from the REPL workflow, to make sure things don't break, the inability to enforce types at compile time introduces the need for more testing, which is annoying.

When developing the puzzle solution, I was accidentally translating between different collection types. This ended up because I was mixing up cons and conj, and because range produces lists instead of vectors, implicitly creating lists for me, etc. And I got null pointer exceptions--that's frustrating.

In the end, I know the experience would have been much better with a complete development workflow and an experienced lisper beside me. I haven't wrestled with the language enough to appreciate its benefits. That said, I think I'll stick with the ML family of functional languages for a bit.

Clone this wiki locally