clojuremacrosclojure-testing

Threading arrow private defns in clojure.test


Consider the following functions in an MVE (minimal viable example) namespace from a fresh lein new app arrow-mve. The function extract-one is public, and the function extract-two is private. I've included the main- function just for completeness and for the remote possibility that it's entailed in my problem:

(ns arrow-mve.core
  (:gen-class))

(defn extract-one [m]
  (-> m :a))

(defn- extract-two [m]
  (-> m :a))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

In my parallel test namespace, I can test these functions as follows. I can test the public function extract-one either by a direct call or using the arrow-threading macro ->. Also notice that I have no problem referring to the private function, extract-two, by its full Var in a direct call. These tests pass:

(ns arrow-mve.core-test
  (:require [clojure.test :refer :all]
            [arrow-mve.core :refer :all]))

(deftest test-one-a
  (is (= 1 (extract-one {:a 1, :b 2}))))

(deftest test-one-b
  (is (= 1 (-> {:a 1, :b 2}
               extract-one))))

(deftest test-two-a
  (is (= 1 (#'arrow-mve.core/extract-two
            {:a 1, :b 2}))))

But I get a compile error when I attempt to call the private function extract-two with the arrow macro:

(deftest test-two-b
  (is (= 1 (-> {:a 1, :b 2}
               #'arrow-mve.core/extract-two))))

$ lein test
Exception in thread "main" java.lang.RuntimeException: Unable to resolve 
  var: arrow.mve.core/extract-two in this context, compiling:
(arrow_mve/core_test.clj:10:12)
  at clojure.lang.Compiler.analyzeSeq(Compiler.java:6875)
  at clojure.lang.Compiler.analyze(Compiler.java:6669)
  at clojure.lang.Compiler.analyze(Compiler.java:6625)

Things get more strange when I make the test a little more complex.

(deftest test-two-b
  (is (= {:x 3.14, :y 2.72}
         (-> {:a {:x 3.14, :y 2.72}, :b 2}
             #'arrow-mve.core/extract-two))))

$ lein test
Exception in thread "main" java.lang.ClassCastException: 
  clojure.lang.PersistentArrayMap cannot be cast to clojure.lang.Symbol, 
  compiling:(arrow_mve/core_test.clj:18:10)
at clojure.lang.Compiler.analyzeSeq(Compiler.java:6875)
at clojure.lang.Compiler.analyze(Compiler.java:6669)
at clojure.lang.Compiler.analyzeSeq(Compiler.java:6856)

Again, the test passes in the direct-call form:

(deftest test-two-b
  (is (= {:x 3.14, :y 2.72}
         (#'arrow-mve.core/extract-two
          {:a {:x 3.14, :y 2.72}, :b 2}))))

I suspect that the problem is a limitation of macro-chaining through deftest, is, the reader macro #' for Var, and the arrow macro, and wondered if it was by design or a potential bug. Of course, in my real application (not this MVE), I have long and deep call chains that make using the arrow macros highly desirable.


Solution

  • Here is the answer (different ns):

    Main namespace:

    (ns clj.core
      (:require [tupelo.core :as t] ))
    (t/refer-tupelo)
    
    (defn extract-one [m]
      (-> m :a))
    
    (defn- extract-two [m]
      (-> m :a))
    

    Testing namespace:

    (ns tst.clj.core
      (:use clj.core
            clojure.test )
      (:require [tupelo.core :as t]))
    (t/refer-tupelo)
    
    (deftest test-one-a
      (is (= 1 (extract-one {:a 1, :b 2}))))
    
    (deftest test-one-b
      (is (= 1 (-> {:a 1, :b 2}
                   extract-one))))
    
    (deftest test-two-a1
      (is (= 1 (#'clj.core/extract-two {:a 1, :b 2}))))
    
    ;(deftest test-two-b
    ;  (is (= 1 (-> {:a 1, :b 2}
    ;               clj.core/extract-two))))  ; fails: not public
    
    ;(deftest test-two-b1
    ;  (is (= 1 (-> {:a 1, :b 2}
    ;               #'clj.core/extract-two))))
    ;     fails: can't cast PersistentArrayMap to Symbol
    
    (deftest test-two-b
      (is (= 1 (-> {:a 1, :b 2} 
                   (#'clj.core/extract-two)))))  ; works
    

    The answer is that the var reference needs to be inside parentheses. The thread macros all have a test of the form (pseudocode):

    (if (not (list? form))
      '(form)
      form)
    

    So a form like

    (-> 1
        inc)
    

    is transformed into

    (-> 1
        (inc))
    

    before the rest of the threading occurs. The if test seems to be failing for you since the var is not a symbol. Enclosing the var in a list as a function call fixes the problem.

    I prefer to always enclose the function calls in threading forms in parentheses, and not use any "naked" functions even though it is normally allowable:

    (-> 1
        (inc)    ; could have typed "inc" w/o parens
        (* 2))   ; must use parens since more than 1 arg
    ;=> 4