简体   繁体   中英

How do I spec higher order function arguments in Clojure?

Let's say I have a function that takes a function and returns a function that applies any arguments it is given to the passed in function and puts the result in a vector (it's a noddy 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 instrument the box function

(spec-test/instrument)

and call box 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 understand the error correctly then it's taking the any? predicate and generating a PersistentVector for the test, which clojure.core/+ obviously can't use. This means I can get it to work by changing box's argument function spec to be

(spec/fspec :args (spec/* number?)
            :ret number?)

but what if I want to use box for both clojure.core/+ and clojure.string/lower-case?

NB To get spec to work 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 the following imports

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

I don't think you can express this function's type with clojure.spec. You would need type variables to be able to write something like (here using a Haskell-style signature)

box :: (a -> b) -> (a -> [b])

That is, it's important that you be able to "capture" the spec of the input function f and include parts of it in your output spec. But there is no such thing in clojure.spec as far as I know. You can also see that clojure.spec's list of specs for built-in functions does not define a spec for, for example, clojure.core/map , which would have the same problem.

Like @amalloy's answer says, the type (spec) of your Higher Order Function's return value depends on the argument you gave it. If you provide a function that can operate on numbers, then the function the HOF returns can also operate on numbers; if it works on strings, then strings, and so on. So, you would need to somehow inherit/reflect on the (spec of the) argument function to provide a correct output spec for the HOF, which I cannot think how.

In any case, I would opt to create separate functions (aliases) for different use cases:

(def any-box box)

(def number-box box)

Then, you can spec these independently:

(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 specs work with instrument as expected:

(spec-test/instrument)

(number-box +)
(any-box list)

Of course, writing a spec for every use case may be quite an effort, if you have many of them.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM