How do I specify higher order function arguments in Clojure?
Let's say I have a function that takes a function and returns a function that takes whatever arguments it is passed to the passed function and puts the result in a vector (this is an example example, but will hopefully illustrate my point).
(defn box [f]
(fn [& args]
[(apply f args)]))
I think the spec for the box function looks like this:
(spec/fdef box
:args (spec/cat :function (spec/fspec :args (spec/* any?)
:ret any?))
:ret (spec/fspec :args (spec/* any?)
:ret (spec/coll-of any? :kind vector? :count 1)))
If I then use the box function
(spec-test/instrument)
and the call field with clojure.core / + I get an exception
(box +)
ExceptionInfo Call to #'user/box did not conform to spec:
In: [0] val: ([]) fails at: [:args :function] predicate: (apply fn), Cannot cast clojure.lang.PersistentVector to java.lang.Number
:clojure.spec.alpha/args (#function[clojure.core/+])
:clojure.spec.alpha/failure :instrument
:clojure.spec.test.alpha/caller {:file "form-init4108179545917399145.clj", :line 1, :var-scope user/eval28136}
clojure.core/ex-info (core.clj:4725)
If I understood the error correctly, then he took some? predicate and generate PersistentVector for test that clojure.core / + obviously cannot use. This means that I can make it work by changing the spec of the box argument function to be
(spec/fspec :args (spec/* number?)
:ret number?)
but what if i want to use field for clojure.core / + and clojure.string / lower-case?
NB To get a spec for working in the REPL I need
:dependencies [[org.clojure/clojure "1.9.0-alpha16"]]
:profiles {:dev {:dependencies [[org.clojure/test.check "0.9.0"]]}}
:monkeypatch-clojure-test false
in project.clj and following imports
(require '[clojure.spec.test.alpha :as spec-test])
(require '[clojure.spec.alpha :as spec])
source to share
I don't think you can express this type of function with clojure.spec. You will need a variable type to be able to write something like (here using Haskell type signature)
box :: (a -> b) -> (a -> [b])
That is, it is important that you can "grab" the specification of the input function f and include it in your output specification. But there is no such thing in clojure.spec as far as I know. You can also see that clojure.spec list of specs for inline functions does not define a spec for, for example clojure.core/map
, which would have the same problem.
source to share
As @ amalloy's answer says that the type (spec) of your higher order function return value depends on the argument you gave it. If you provide a function that can work with numbers, then the function returned by the HOF can also work with numbers; if it works on strings, then strings, etc. So you need to somehow inherit / reflect the function of the argument (spec of the) to provide the correct output specification for HOF which I can't think of.
In any case, I would prefer to create separate functions (aliases) for different use cases:
(def any-box box)
(def number-box box)
Then you can specify them yourself:
(spec/fdef any-box ;... like your original spec for box
(spec/fdef number-box
:args (spec/cat :function (spec/fspec :args (spec/* number?)
:ret number?))
:ret (spec/fspec :args (spec/* number?)
:ret (spec/coll-of number? :kind vector? :count 1)))
The spectra are working with the instrument as expected:
(spec-test/instrument)
(number-box +)
(any-box list)
Of course, writing a BOM for each use case can be quite tricky if you have a lot of them.
source to share