Lisps are a horrible language family because it is entirely up to the programmer to decide how to think about their problem. This problem becomes worse when the poor programmer tries to use libraries - because then they have to figure out if the foreign ideas of other programmers are good or bad.
Here stands my compiled opinions on how to think about Clojure programs. May I not forget to read it from time to time.
I installed babashka and it didn’t work out very well - it proved
clunky to integrate with deps.edn
. Nevertheless, for
projects that can be done with no dependencies (rare for me, since I
want access to orchestra
) bb
avoids the slow
startup time of clojure
. It requires a local
bb.edn
rather than a deps.edn
. I installed
babashka with:
$ wget https://raw.githubusercontent.com/babashka/babashka/master/install
$ chmod +x install
$ ./install /home/owen/bin
To install certain key libraries, I set up a user-wide deps file in
$XDG_CONFIG_HOME/clojure/deps.edn
aka
~/.config/clojure/deps.edn
. CIDER in Emacs doesn’t know
about this file, so I will typically need to duplicate it at the project
level to get everything working.
:deps {cheshire/cheshire {:mvn/version "5.10.0"} ; namespace: cheshire.core
{:mvn/version "1.0.0"}
org.clojure/core.match {:mvn/version "0.9.7"}
clojure-lanterna/clojure-lanterna {:mvn/version "2021.01.01-1"}
orchestra/orchestra {:mvn/version "1.0.206"}
org.clojure/tools.cli {:local/root "/home/owen/devel/x-clojure/opar/"}
opar/opar { }}
The last one is a set of personal functions I use a lot. Mainly
imperative
which combines let, do and cond in an imperative
style. You can see the code. This is a later
development of the function which started as a variant of
cond
.
I needed to put a simple deps.edn in the opar
folder -
{:paths ["."]}
. It took a bit of mucking around to verify
that this worked. It looked like an absolute path was needed for a
while. Eventually a caching problem was surfaced that could be resolved
by running clj -Sverbose -Spath -Sforce | sed -e 's/:/\n/g'
(which obviously does more than that, it is useful incantation). Deps
remains confusing and questionably documented.
Clojure has a serious problem in shell scripts. A shell script will
traditionally start with a shebang line like
#!/usr/bin/env clojure
. This is problematic. It will try to
find a deps.edn
in the file where the script is being
executed, which is probably not where the script lives.
There may be some way to overrule this behaviour by loading the deps
in with the interpreter like
#!/use/bin/env -S clojure -Sdeps /home/owen/the/deps.edn
. I
just got an
Error while parsing option "--config-data /home/owen/the/deps.edn": java.lang.RuntimeException: Invalid token: /home/owen/the/deps.edn
so that didn’t work for me.
It looks like the easy way is a bash script, which starts the Clojure
interpreter in the directory with the deps.edn
, then pass
it the directory that the bash script was run in. Ugly, but it seems
this is a tradeoff that lets me keep orchestra
so I’m
willing to make it.
This is also an opportunity to more completely set up the script’s environment, incorporating lessons from 5.1.
#!/usr/bin/env bash
# REMINDER: We're doing this in a bash script because the clojure interpreter
# needs to be executed in the same directory as `deps.edn`
# Where the script was run from
RUN_DIR=$PWD
# Where the clojure interpreter should start, absolute path
SCRIPT_PATH="`dirname \"$0\"`"
SCRIPT_PATH=`cd $SCRIPT_PATH && pwd`
# The clojure script will need to work with the *command-line-args* to work out
# what directory it should be operating in.
cd $SCRIPT_PATH
clojure check.clj $RUN_DIR
# Execute the interpreter in the script directory, pass it the working
# directory and any other arguments.
# The clojure script will need to work with the *command-line-args* to work out
# what directory it should be operating in. `clojure` seems like it might be
# sensitive to argument order.
cd $SCRIPT_PATH
clojure -i main.clj --report stderr -- $RUN_DIR $@
Use tools.cli for creating a CLI interface.
:deps {org.clojure/tools.cli {:mvn/version "1.0.206"}}} {
There is a minor practical problem to overcome since the command line
parameters won’t be set up when doing interactive development. The
command line args aren’t even nil
if CIDER is being used
and might look something like
("--middleware" "[cider.nrepl/cider-middleware]")
. If there
is an elegant way it isn’t obvious so solve with a crude approach from
the opar
namespace.
defn command-line-args
("Returns clojure.core/*command-line-args*. However if there is evidence that
this code is being executed in a CIDER REPL then instead it will evaluate to
`default`. CIDER is detected by checking if the command line args load
CIDER's middleware."
[defaults]cond
(not= *command-line-args*
("--middleware" "[cider.nrepl/cider-middleware]")) *command-line-args*
'(:else defaults))
There is a second unhelpful situation - sometimes called Buridan’s Ass - where it is difficult to make a deterministic choice between equal alternatives. Interfaces can be a bit like that for me, there are a lot of options and it isn’t obvious how to be systemic about making choices. What does seem clear is that making some choice is better than thinking for too long. Here is my flow chart for choosing an interface:
Consider babashka
first for simple utilities that don’t
need spec
.
#!/usr/bin/env bb
(println "I am a script!")
#!/usr/bin/env clojure
ns template
(:require [clojure.tools.cli :refer [parse-opts]])
(:gen-class))
(
def cli-options
(;; An option with a required argument
"-p" "--port PORT" "Port number"
[[:default 80
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
;; A non-idempotent option (:default is applied first)
"-v" nil "Verbosity level"
[:id :verbosity
:default 0
:update-fn inc]
;; A boolean option defaulting to nil
"-h" "--help"]])
[
println (parse-opts *command-line-args* cli-options)) (
It isn’t very obvious how to use it from the README, and by the time you remember how to use it you’ll have lost all motivation to get anything done. Don’t bother.
Use Lanterna for creating a terminal user interface. Well documented and easy to use.
:deps {clojure-lanterna/clojure-lanterna {:mvn/version "0.9.7"}}} {
It isn’t easy to outdo Java’s Sling - it works everywhere and is
comfortingly basic. Best to use a wrapper-like library to make it a bit
terser; try Seesaw.
seesaw
is an instant hit with a great code style and well
documented. Dave Ray, who wrote it,
obviously knows how to build a GUI.
:deps {seesaw/seesaw {:mvn/version "1.5.0"} ; namespace: seesaw.core}} {
Note that these libraries are all added in my user-wide
deps.edn
that is mentioned under the 2.1 heading.
Despite initial impressions, there is actually useful information
about deps.edn
in the clojure reference pages. Pretend that
the document was written spiralling out from the centre; start with the
“deps.edn sources” heading.
Note that the base data structure here is a map and therefore keys must be unique.
There is but little rhyme or reason to the sub-keys of :deps. Copy someone elses work or just arbitrarily name stuff something/or-other. Include {:paths [“.”]} in your deps.edn to include the current folder in the classpath.
This project would, by itself, get Ambrose Bonnaire-Sergeant into a hall of fame. Unfortunately the effort is spoilt by a few weaknesses:
core.typed
and
its successor typedclojure
.
typedclojure
github
repository?update-in
get typed?{:a 42}
- is the :a
part of a type or just
data? Not the type checker’s fault.spec
?I honestly wanted to use this family of libraries. At the end of the day the ergonomics didn’t seem to be smooth enough but persisting paid off and it became apparent that I was too nervous about nailing down the keys of my maps.
Use orchestra. The clojure devs don’t seem to have implemented using the :ret key - it is clear why spec is an alpha library!
:deps {orchestra/orchestra {:mvn/version "2021.01.01-1"}}} {
ns example
(:require
(:as s]
[clojure.spec.alpha :refer [defn-spec]]
[orchestra.core test :as st])) [orchestra.spec.
It is a good idea to instrument each function after declaration.
ns named-space
(:require [orchestra.core :refer [defn-spec]]
(test :as st]
[orchestra.spec.:as s]))
[clojure.spec.alpha
;; later
(st/instrument)
defn-spec
stresses clj-kondo, the usual linter. Add
{:lint-as {net.danielcompton.defn-spec-alpha/defn schema.core/defn}}
to the project’s .clj-kondo/config.edn
file
Consider a collection of elements
[{:a 1} {:a 2} {:a 3 :b 1}]
with the intent of creating a
spec to ensure that this is a collection of maps with an :a
key.
Here are a few attempts at achieving this.
ns example (:require [clojure.spec.alpha :as s]))
(
;; I could use (s/keys). But I won't since all that matters is that the objects
;; meet a spec
:example/entry
(s/def contains? % :a))
#(
;; Method 1
;; The problem with this implementation is the messages print the entire
;; collection x when there is a problem. It is better to just print the failed
;; entry. Or better yet jsut the part of the failed entry that failed to meet
;; the spec.
:example/spec
(s/def
(s/andvector?
fn [x] (every? (partial s/valid? :example/entry) x))))
(
:example/spec [{:a 1} {:b 2}])
(s/explain ;; => [{:a 1} {:b 2}] - failed: (fn [x] (every? (partial valid? :example/entry) x))
;; Method 2
;; Right idea? We might expect this approach to be buggy since (s/coll-of)
;; takes a predicate and :example/entry is a keyword. However, there is an
;; undocumented feature where coll-of can be passed spec references.
;; This approach has risks. coll-of will conform the elements of the vector
;; being passed and so may transform the data if more clauses are added to the
;; and.
:example/spec
(s/def
(s/andvector?
:example/entry)
(s/coll-of
))
:example/spec [{:a 1} {:b 2}])
(s/explain ;; => {:b 2} - failed: (contains? % :a) in: [1] spec: :example/entry
;; Method 3
;; Superficially this looks ok; but it is basically buggy. (s/every) is
;; misnamed and doesn't check that every element of the collection meets the
;; spec - it only takes a random sample (?!). It also has undocumented
;; behaviour that it can take a spec-reference. Best avoided.
:example/spec
(s/def
(s/andvector?
:example/entry)
(s/every
))
:example/spec [{:a 1} {:b 2}])
(s/explain ;; => {:b 2} - failed: (contains? % :a) in: [1] spec: :example/entry
This specific example suggests
(s/coll-of :example/entry :kind vector?)
is the most
appropriate spec to use.
There is an ambiguity that is important to recognise - a predicate like string? and a spec like (coll-of string?) are very different. A spec is in fact some sort of Java class and doesn’t seem to implement IFn (ie, can’t be called). That means a predicate is a spec, but a spec cannot be used as a predicate.
This creates a slight awkwardness between s/and
,
and
and every-pred
. Since predicated are also
specs, s/and
can mix the two. But since some spec
conformances transform the data this function has unexpected properties.
For example, (s/and string? ::something)
and
(s/and ::something string?)
are completely different specs,
and what is valid for one might be invalid for the other. After
conforming to ::something
the data might be structured
differently.
every-pred
is a bit more reasonable, applying a set of
predicates in parallel. But it is poorly integrated into the spec
library, so on failure the specific failing predicate isn’t
properly identified. This undermines the basic value proposition I’m
looking for in spec which is clear error messages describing how
functions are failing. It also requires specs to be transformed back
into predicates: #(s/valid? ::something %)
. Forgetting this
produces crazy error messages as the spec library tries to explain that
a spec is not a function.
::somespec
(s/def
(every-predvector? ; this line is OK. (vector?) is a predicate.
;; This fails - it treats a spec as a predicate. This spec is not a predicate.
::otherspec %)) ; this like fails.))
(s/coll-of #(s/valid?
;; A sample of a crazy error message when every-pred tries to use a spec as a predicate.
class clojure.spec.alpha$every_impl$reify__2255 cannot be cast to class clojure.lang.IFn (clojure.spec.alpha$every_impl$reify__2255 and clojure.lang.IFn are in unnamed module of loader 'app')
and
from Clojure core is not useful here, being inferior
to every-pred. It just exists to confuse us by having wildly different
semantics to s/and
. s/&
and the regex
operators get little acknowledgement here. It isn’t clear to me how they
work and it is late at night as I write this, having been fighting with
the other options all evening. Regex probably aren’t a solution, so I’m
not touching them.
I had reason to be working with some JSON objects that were read and manipulated in Clojure. I tried spec but struggled with the assumption that map keys were keywords, not other objects (like strings, which is hugely common ion JSON).
After careful thought, I concluded that it was better to use json-schema to validate these objects. The =luposlip/json-schema` library claims to generate JSON schemas from simple Clojure data structures. It does work, although the README file leaves a little to be desired.
;; Generate a schema
require 'json-schema.infer)
(require '[json-schema.core :as json-schema])
(
def json-object {:the "json" "object" 111})
(
"schema.json" (json-schema.infer/infer->json {:title "Good Schema"} json-object))
(spit
; Fails, 2 should be "2"
(json-schema/validate:title "yes"} json-object)
(json-schema.infer/infer->json {"the" 2 "object" 3}
{ )
:deps {luposlip/json-schema {:mvn/version "0.3.2"}}
{:mvn/repos {;; relies on everit-org/json-schema which is distributed on jitpack
"JitPack" {:url "https://jitpack.io"}}}
clojure.zip/vector-zip
. If you are using a
vector, the index of an element in the vector probably matters. A zipper
does not neatly catch this concept.:deps {org.clojure/core.match {:mvn/version "1.0.0"}
{:mvn/version "5.10.0"} ; namespace: cheshire.core
cheshire/cheshire { }}
cheshire
takes the place of data.json
due
to being bundled with babashka.Consider:
(System/setProperty "clojure.main.report" "report")
(alternative is file: report stacktraces to /tmp file).Emacs w/ cider & smartparen.
https://ianyepan.github.io/archives has a few great
posts. Set up use-package
and:
(use-package cider)
(use-package clojure-mode:mode "\\.clj\\'")
(use-package company)
(use-package rainbow-delimiters:hook ((clojure-mode . rainbow-delimiters-mode)))
(use-package blamer:hook ((clojure-mode . blamer-mode)))
;; We are going to install lsp-mode later. Run
;; M-x lsp-install-server RET clojure-lsp
(use-package lsp-mode:hook ((clojure-mode) . lsp-deferred)
:commands lsp)
So for reasons various I want to use Clojure to write a shell script. The script is fairly simple, say:
#!/usr/bin/env clojure
throw (new java.lang.Exception "!")) (
But when I run it:
$ script.clj
WARNING: When invoking clojure.main, use -M
Syntax error compiling at (./script.clj:1:1).
!
Full report at:
/tmp/clojure-16000908815811689537.edn
That report is troubling! I want my errors logged straight to the
console. Having to open up a tmp file be too slow; I make a lot of
mistakes. A little
reading suggests that I want to set
-Dclojure.main.report=stderr
.
$ script.clj -Dclojure.main.report=stderr
WARNING: When invoking clojure.main, use -M
Syntax error compiling at (./script.clj:1:1).
!
Full report at:
/tmp/clojure-16000908815811689537.edn
That didn’t work. But I’ve always wanted to know what these stack
traces mean, so I open up
/tmp/clojure-16000908815811689537.edn
and use it as a guide
to main.clj.
The journey starts at [clojure.main main "main.java" 40]
which is
defn root-cause
("Returns the initial cause of an exception or error by peeling off all of
its wrappers"
:added "1.3"}
{
[^Throwable t]loop [cause t] ; <= Line 40
(if (and (instance? clojure.lang.Compiler$CompilerException cause)
(not= (.source ^clojure.lang.Compiler$CompilerException cause) "NO_SOURCE_FILE"))
(
causeif-let [cause (.getCause cause)]
(recur cause)
( cause))))
This must be the bottom of the stack for doing error triage. Scanning
up the stack, the next interesting frame is
[clojure.main$main doInvoke "main.clj" 616]
- not very
useful, that is (defn main ...)
- and one level higher is
[clojure.main$main invokeStatic "main.clj" 664]
. Aha!
; ... in (defn main ...
:main-opt
try
(; <= Line 664
((main-dispatch opt) args inits) catch Throwable t
(:target (get flags "report" (System/getProperty "clojure.main.report" "file")))
(report-error t 1)))))
(System/exit
; ...
Clojure must be accessing the property at the last minute with
(System/getProperty)
. I bet there is a
System/setProperty.
#!/usr/bin/env clojure
"clojure.main.report" "report")
(System/setProperty
throw (new java.lang.Exception "!")) (
$ ./script.clj
WARNING: When invoking clojure.main, use -M
{:clojure.main/message
"Syntax error compiling at (./script.clj:5:1).\n!\n",
:clojure.main/triage
{:clojure.error/phase :compile-syntax-check,
:clojure.error/line 5,
:clojure.error/column 1,
:clojure.error/source "script.clj",
:clojure.error/path "./script.clj",
:clojure.error/class java.lang.Exception,
:clojure.error/cause "!"},
:clojure.main/trace
{:via
# continues ...
And now exceptions are available.
Clojure specs are actually a debugging nightmare for someone developing in CIDER. It is very difficult to figure out why a spec is misbehaving. For example
::example
(s/def
(s/andmap?
contains? % :a)
#(% :a))
#(int? (
(s/coll-of int?)
))
::example {:a 1}) (s/explain
The problem here is clear and the spec is impossible to satisfy. The
spec has to be a map that contains something like {:a 1}
and when that is viewed as a collection to check the
s/coll-of
that entry of the map will be
[:a 1]
, which is not an int. The programmer has made a
mistake. They probably meant to check an element of the map.
The problem here is that there is nothing to instrument to step through the spec and find out why it is going through the map entries and expecting them to be integers. The situation is confusing for someone who doesn’t already understand the problem. Is it the last or second-to-last line of the spec that is the problem? It becomes harder to figure this out the larger the spec is. Bugs here could be hard to find.
We can’t instrument the function because specs are checked before
invocation or after evaluation. We can’t instrument the spec because it
is data and never executed. We can’t really instrument the checking
functions because we don’t know where they are and it isn’t obvious
CIDER can instrument clojure.core
.
You can look at the code for conforming coll-of. It doesn’t look fun to step through that. This might have to be shelved as an unresolved problem. No complex specs for me.
Sequences are to be made up of specable components. If the sequence
is specced ::seq
then the elements are speced
::seq+
.