Search code examples
clojureruntime-error

Find out where the error happened in Clojure


For the most part I understand what Clojure is telling me with it's error messages. But I am still clueless as to find out where the error happened.

Here is an example of what I mean

(defn extract [m]
  (keys m))

(defn multiple [xs]
  (map #(* 2 %) xs))

(defn process [xs]
  (-> xs
      (multiple)     ; seq -> seq
      (extract))) ; map -> seq ... fails

(process [1 2 3])

Statically typed languages would now tell me that I tried to pass a sequence to a function that expects a map on line X. And Clojure does this in a way:

ClassCastException java.lang.Long cannot be cast to java.util.Map$Entry

But I still have no idea where the error happened. Obviously for this instance it's easy because there are just 3 functions involved, you can easily just read through all of them but as programs grow bigger this gets old very quickly.

Is there a way find out where the errors happened other than just proof reading the code from top to bottom? (which is my current approach)


Solution

  • You can use clojure.spec. It is still in alpha, and there's still a bunch of tooling support coming (hopefully), but instrumenting functions works well.

    (ns foo.core
      (:require
       ;; For clojure 1.9.0-alpha16 and higher, it is called spec.alpha
       [clojure.spec.alpha :as s]
       [clojure.spec.test.alpha :as stest]))
    
    
    ;; Extract takes a map and returns a seq
    (s/fdef extract
      :args (s/cat :m map?)
      :ret seq?)
    
    (defn extract [m]
      (keys m))
    
    
    ;; multiple takes a coll of numbers and returns a coll of numbers
    (s/fdef multiple
      :args (s/cat :xs (s/coll-of number?))
      :ret (s/coll-of number?))
    
    (defn multiple [xs]
      (map #(* 2 %) xs))
    
    
    (defn process [xs]
      (-> xs
          (multiple)     ; seq -> seq
          (extract))) ; map -> seq ... fails
    
    ;; This needs to come after the definition of the specs,
    ;; but before the call to process.
    ;; This is something I imagine can be handled automatically
    ;; by tooling at some point.
    (stest/instrument)
    
    ;; The println is to force evaluation.
    ;; If not it wouldn't run because it's lazy and
    ;; not used for anything.
    (println (process [1 2 3]))
    

    Running this file prints (among other info):

    Call to #'foo.core/extract did not conform to spec: In: [0] val: (2
    4 6) fails at: [:args :m] predicate: map?  :clojure.spec.alpha/spec
    #object[clojure.spec.alpha$regex_spec_impl$reify__1200 0x2b935f0d
    "clojure.spec.alpha$regex_spec_impl$reify__1200@2b935f0d"]
    :clojure.spec.alpha/value ((2 4 6)) :clojure.spec.alpha/args ((2 4
    6)) :clojure.spec.alpha/failure :instrument
    :clojure.spec.test.alpha/caller {:file "core.clj", :line 29,
    :var-scope foo.core/process}
    

    Which can be read as: A call to exctract failed because the value passed in (2 4 6) failed the predicate map?. That call happened in the file "core.clj" at line 29.

    A caveat that trips people up is that instrument only checks function arguments and not return values. This is a (strange if you ask me) design decision from Rich Hickey. There's a library for that, though.