Search code examples
clojureclojure.spec

clojure-spec: Unable to get function's postcondition right


I'm trying clojure spec on a simple function that computes the "neighbours" of a (row,col) position in a square matrix. For example for the 4x4 matrix given below, the neighbours of cell (1,1) shall be: (0,1), (1,0), (1,2), (2,1). The neighbours of cell (4,3), which is not even in the matrix' range, shall be (3,3) etc.

4x4 square matrix and two positions with their neighbours

The function's input is the size of the matrix and the (row,col) of the position of interest. The output is a collection of (row,col) of the neighbours. This collection can be empty if there are no neighbours.

This problem can be found in "The Joy of Clojure, 2nd editions, page 94; but this code is modified because the original was too compact for me. Then I tried to spec it and check the spec in the :pre and :post parts.

However, I don't get the :post part to work. When I run the test cases, I get:

java.lang.ClassCastException: java.lang.Boolean cannot be cast
to clojure.lang.IFn

What to change?

(require '[clojure.spec.alpha :as s]
         '[clojure.test       :as t])

; ===
; Specs
; ===

(s/def ::be-row-col
       (s/coll-of integer? :count 2 :kind sequential?))
(s/def ::be-square-matrix-size
       (s/and integer? #(<= 0 %)))
(s/def ::be-row-col-vector
       (s/and (s/coll-of ::be-row-col) (s/int-in-range? 0 5 #(count %))))

; ===
; Function of interest
; ===

(defn neighbors [sqmsz rc]

     {:pre  [(s/valid? ::be-row-col rc)
             (s/valid? ::be-square-matrix-size sqmsz)] 
      :post [(s/valid? ::be-row-col-vector %)]
     }

     (let [ cross             [[-1 0] [1 0] [0 -1] [0 1]]
            in-sq-matrix?     (fn [x]
                                  (and (<= 0 x) (< x sqmsz)))
            in-sq-matrix-rc?  (fn [rc]
                                  (every? in-sq-matrix? rc))
            add-two-rc        (fn [rc1 rc2]
                                  (vec (map + rc1 rc2)))
            get-rc-neighbors  (fn [rc]
                                  (map (partial add-two-rc rc) cross)) ]
        (filter in-sq-matrix-rc? (get-rc-neighbors rc))))

; ===
; Put a collection of [row col] into an expected form
; ===
; this is used to run the test code

(defn formify [rc-coll]
   (let [ cmp (fn [rc1 rc2]
                  (let [ [r1 c1] rc1
                         [r2 c2] rc2 ]
                     (cond (< r1 r2) -1  ; sort by row
                           (> r1 r2) +1
                           (< c1 c2) -1  ; then by column
                           (> c1 c2) +1
                           true       0))) ]
      (vec (sort cmp rc-coll))))

; ===
; Testing
; ===

(defn test-nb [ sqmsz rc expected txt ]
   (do
      (t/is (= (formify (neighbors sqmsz rc)) expected) txt)
   ))

(test-nb  0  [0 0]  []  "Zero-size matrix, outside #1")    
(test-nb  0  [1 1]  []  "Zero-size matrix, outside #2")    
(test-nb  1  [0 0]  []  "One-size matrix, inside")    
(test-nb  1  [1 0]  [[0 0]]  "One-size matrix, outside")    
(test-nb  5  [0 0]  [[0 1] [1 0]]  "Testing top left")    
(test-nb  5  [1 0]  [[0 0] [1 1] [2 0]]  "Testing left edge")    
(test-nb  5  [1 1]  [[0 1] [1 0] [1 2] [2 1]]  "Testing middle #1")    
(test-nb  5  [2 2]  [[1 2] [2 1] [2 3] [3 2]]  "Testing middle #2")    
(test-nb  5  [3 3]  [[2 3] [3 2] [3 4] [4 3]]  "Testing middle #3")    
(test-nb  5  [4 4]  [[3 4] [4 3]]  "Testing btm right")    
(test-nb  5  [5 5]  []  "Testing outside #1")    
(test-nb  5  [5 4]  [[4 4]]  "Testing outside #2")    
(test-nb  5  [4 3]  [[3 3] [4 2] [4 4]]  "Testing btm edge")

Solution

  • You're just missing the # prefix to make your anonymous function in the :post condition. The post condition needs to be a function that can take the output of the subject function's invocation.

    :post [#(s/valid? ::be-row-col-vector %)]
    

    Could also be rewritten as:

    :post [(fn [o] (s/valid? ::be-row-col-vector o))]
    

    But depending on your use case, you may want to look into function specs and instrument as an alternative to :pre and :post conditions. I wrote more examples here.