speculoos.function-specs
This namespace provides facilities to apply specifications to and validate functions. Roughly speaking, trying to replicate the instrumentation and testing capabilities of spec.alpha .
Speculoos explores a few different styles of function specification:
- Explicitly providing the specification during explicit validation.
- Implicitly providing the specification via metadata, explicitly validating.
- Implicitly providing the specification via metadata, implicitly validating via instrumentation.
Warning: Several actions performed by functions in this namespace mutate state, and are brittle. Use this namespace with the understanding that it is very much a proof-of-concept.
assoc-metadata-f!
(assoc-metadata-f! f k v)
Associate metadata key k
, value v
to the var of function f
. Returns the metadata map. See dissoc-metadata-f! for the inverse operation.
Example:
(defn foo [] true)
(assoc-metadata-f! foo :assoced 'bar!)
(:assoced (meta #'foo)) ;; => 'bar!
dissoc-metadata-f!
(dissoc-metadata-f! f k)
Dissociate metadata key k
from the var of function f
. Returns the metadata map. See assoc-metadata-f! for the inverse operation.
Example:
(defn foo [] true)
(assoc-metadata-f! foo :assoced 'bar!)
(:assoced (meta #'foo)) ;; => 'bar!
(dissoc-metadata-f! foo :assoced)
(:assoced (meta #'foo)) ;; => nil
exercise-fn
(exercise-fn f)
(exercise-fn f n)
Exercises the function f
by applying it to n
(default 10
) generated samples of its scalar argument specification, residing at :speculoos/arg-scalar-spec
in its metadata.
Returns a sequence of tuples of [args ret]
. Does not currently handle higher-order-functions.
If n
is :canonical
, only one data set is produced, consisting of the predicates’ canonical values.
See data-from-spec for details on predicates.
Example:
(defn foo [x kw] (str (+ 100 x) "is not equivalent to " kw))
(foo 5 :bar) ;; "105 is not equivalent to :bar"
(inject-specs! foo {:speculoos/arg-scalar-spec [int? keyword?]})
(exercise-fn foo 3)
;; => ([[76 :i-:!7S] "176 is not equivalent to :i-:!7S"]
;; [[-381 :W] "-281 is not equivalent to :W"]
;; [[-940 :LS1-:i] "-840 is not equivalent to :LS1-:i"])
Examples with canonical values:
(defn bar
{:speculoos/arg-scalar-spec [int? ratio? float?]}
[x y z]
(+ x y z))
(exercise-fn bar :canonical)
;; => ([[42 22/7 1.23] 46.372857142857136])
inject-specs!
(inject-specs! f specs)
Given function f
, associates scalar and collection specifications for arguments and return values. specs
is a map, whose only recognized pseudo-qualified keys are contained in recognized-spec-keys. No warnings are given for key-vals that are not recognized and thus not injected. Returns nil
. See unject-specs! for the inverse operation.
Example:
(defn foo [] true)
(inject-specs! foo {:speculoos/ret-scalar-spec boolean?}) ;; => nil
(:speculoos/ret-scalar-spec (meta #'foo)) ;; => boolean?
instrument
(instrument f)
Wrap function f
such that invoking f
implicitly performs argument validation before and return value validation after execution. See wrapping-fn for details.
Implemented by:
- Adding an entry into the instrumentation registry.
- Altering
f
’s root var to wrapped version of itself.
The inverse operation is provided by unstrument.
Warning: This implementation is experimental and brittle. Since it relies on mutating vars, it is sensitive to order of invocation and multiple invocations.
Examples:
(defn add-n-subtract [x y] [(+ x y) (- x y)])
;; `instrument` requires specifications be added *before* instrumentation
(inject-specs! add-n-subtract {:speculoos/arg-scalar-spec [int? ratio?]})
(instrument add-n-subtract)
;; while instrumented, each invocation automatically validates any supplied specification
;; non-satisfied predicates are send to *out*
(with-out-str (add-n-subtract 5 2)) ;; printed to *out* ({:path [1], :datum 2, :predicate ratio?, :valid? false})
;; if all specifications are satisfied, the function returns a value
(add-n-subtract 5 3/2) ;; => [13/2 7/2]
;; remove instrumentation wrapper...
(unstrument add-n-subtract) ;; => {}
;; ...but metadata specifications remain
(:speculoos/arg-scalar-spec (meta #'add-n-subtract)) ;; => [int? ratio?]
;; function resumes normal behavior; non-satisfying arguments are consumed without note
(add-n-subtract 5 2) ;; => [7 3]
(add-n-subtract 5 3/2) ;; => [13/2 7/2]
recognized-spec-keys
A sequence that contains the only allowed pseudo-qualified keys to be added to, or referred from, a function’s metadata. Only governs behavior of utilities provided by this namespace, such as instrument, unstrument, validate-fn-with, and validate-fn. Does not affect anything outside this namespace.
unject-specs!
(unject-specs! f)
Given function f
, dissociates any key-vals contained in recognized-spec-keys. Inverse operation provided by inject-specs!. Returns nil
.
Example:
(defn foo [] true)
(inject-specs! foo {:speculoos/ret-scalar-spec boolean?}) ;; => nil
(:speculoos/ret-scalar-spec (meta #'foo)) ;; => boolean?
(unject-specs! foo) ;; => nil
(:speculoos/ret-scalar-spec (meta #'foo)) ;; => nil
unstrument
macro
(unstrument f)
Un-wrap function f
to its original form that does not implicitly validate arguments nor return values.pre-fn nor post-fn. This is the inverse operation of instrument.
Compliments to whoever originally thought of ‘unstrument’.
validate-argument-return-relationship
(validate-argument-return-relationship arg ret rel)
Validates an argument/return relationship given argument sequence arg
, function return value ret
, and relationship rel
, a map of {:path-argument … :path-return … :relationship-fn …}
. :relationship-fn
is a 2-arity function that presumably tests some relationship between a slice of arg
, passed as the first argument to the relationship function, and a slice of ret
, passed as the second argument to the relationship function. A nil
path indicates a ‘bare’ scalar value (i.e., a non-collection). arg
ought to always be a sequence.
Intended to validate the relationship between a function’s arguments and its return value.
Examples:
(defn doubled? [x y] (= y (* 2 x)))
;; 'bare' function return, uses path `nil`
(validate-argument-return-relationship [42] 84 {:path-argument [0]
:path-return nil
:relationship-fn doubled?})
;; => {:path-argument [0],
;; :path-return nil,
;; :relationship-fn doubled?,
;; :datum-argument 42,
;; :datum-return 84,
;; :valid? true}
(defn reversed? [v1 v2] (= v2 (reverse v1)))
;; argument and return value fail to satisfy relationship
(validate-argument-return-relationship [1 2 3] [1 2 3] {:path-argument []
:path-return []
:relationship-fn reversed?})
;; => {:path-argument [],
;; :path-return [],
;; :relationship-fn reversed?,
;; :datum-argument [1 2 3],
;; :datum-return [1 2 3],
;; :valid? false}
validate-fn
(validate-fn f & args)
Validates function f
in the manner of validate-fn-with, except specifications are supplied by the function’s metadata, addressed by recognized-spec-keys.
Examples:
(defn foo [x s] (+ x (read-string s)))
(foo 7 "8") ;; 15
(def foo-spec {:speculoos/arg-scalar-spec [int? string?]
:speculoos/ret-scalar-spec number?})
(inject-specs! foo foo-spec) ;; => nil
;; supplying valid arguments; function returns
(validate-fn foo 7 "8") ;; 15
;; supplying invalid argument (arg2 is not a string); yields a report
(validate-fn foo 7 8)
;; => ({:path [1], :datum 8, :predicate string?, :valid? false, :fn-spec-type :speculoos/argument}
;; foo was not able to yield a value, therefore the return was not validated
validate-fn-with
(validate-fn-with f specs & args)
Validate the scalar and collection aspects of arguments args
, the return value, and the relationship between the arguments and return value, of function f
. specs
is a map with any permutation of recognized-spec-keys. Returns f
’s results if specs
are fully satisfied, otherwise, returns a report.
Note: Argument and return values satisfying the specifications does not guarantee that the function’s output is correct.
See validate-scalars and validate-collections for details about scalar and collection validation.
See validate-argument-return-relationship for details about validating relationships between the function’s argument and the function’s return value.
Example, validating scalars:
(defn foo [x y] (+ x y))
;; no specifications; return value passes through
(validate-fn-with foo {} 2 3)
;; => 5
;; all scalar specifications satisfied; return value passes through
(validate-fn-with foo
{:speculoos/arg-scalar-spec [int? int?]
:speculoos/ret-scalar-spec int?}
2 3)
;; => 5
;; one argument scalar specification and the return value scalar specification not satisfied
(validate-fn-with foo
{:speculoos/arg-scalar-spec [int? int?]
:speculoos/ret-scalar-spec int?}
2 3.3)
;; => ({:path [1], :datum 3.3, :predicate int?, :valid? false, :fn-spec-type :speculoos/argument}
;; {:path nil, :datum 5.3, :predicate int?, :valid? false, :fn-spec-type :speculoos/return})
Example, validating argument/return value relationship:
;; function to validate
(defn broken-reverse [v] v)
;; function to test if the return collection is a correctly reversed version of the argument collection
(defn reversed? [v1 v2] (= v2 (reverse v1)))
;; yup, it's truly broken
(reversed? [11 22 33] (broken-reverse [11 22 33]))
;; => false
;; `broken-reverse` fails to satisfy the relationship function because it doesn't correctly reverse the argument collection
(validate-fn-with broken-reverse
{:speculoos/argument-return-relationships [{:path-argument [0]
:path-return []
:relationship-fn reversed?}]}
[11 22 33])
;; => ({:path-argument [0],
;; :path-return [],
;; :relationship-fn reversed?,
;; :datum-argument [11 22 33],
;; :datum-return [11 22 33],
;; :valid? false,
;; :fn-spec-type
;; :speculoos/argument-return-relationship})
validate-higher-order-fn
(validate-higher-order-fn f & args)
Evaluates arbitrarily-deep, higher-order function f
to exhaustion (i.e., until it yields a value that is not a function), validating arguments args
and the return value. Halts on exceptions. Caller must supply sufficient &-arg
vectors such that the final output could satisfy the return specification, if it exists.
Specifications are supplied in the top-level function’s metadata. All other lower-level function var metadata is ignored. Specifications must be a member of recognized-spec-keys. Specifications for the top-level function reside in the standard keys. Specifications for lower-level functions are nested into their respective tiers of the :speculoos/hof-specs
key. If an argument collection specification is supplied for a lower-level function, a collection specification must be supplied for all functions ‘above’ that, even if those higher-level specifications are empty.
If every specification is satisfied, then the function’s evaluated value is returned, otherwise a sequence of invalidation reports.
Un-defined behavior if the final yield is nil
or a function.
If you would call
(((f 1 2) :foo :bar) 'a 'b)
then the validation would be
(validate-higher-order-fn f [1 2] [:foo :bar] ['a 'b])
Use (with-meta f m)
for ad hoc specification.
Example:
(defn foo [x y] (fn [w z] (* (+ x w) (- y z))))
(def bar (foo 8 11))
(bar 12 6) ;; 100
(def foo-spec {:speculoos/arg-scalar-spec [int? ratio?]
:speculoos/hof-specs {:speculoos/arg-scalar-spec [int? ratio?]}})
(inject-specs! foo foo-spec) ;; nil
;; even though `foo` could consume these arguments and produce a value,
;; the arguments do not satisfy specification
(validate-higher-order-fn foo [8 11] [12 6])
;; => ({:path [0 1], :datum 11, :predicate ratio?, :valid? false, :fn-tier :speculoos/argument}
;; {:path [1 1], :datum 6, :predicate ratio?, :valid? false, :fn-tier :speculoos/argument})
;; arguments satisfy specification, so `foo` returns the value
(validate-higher-order-fn foo [8 16/3] [12 1/3]) ;; 100N
wrapping-fn
(wrapping-fn f)
Low-level plumbing for instrument. See validate-fn-with for details.
Returns a function that will sequentially:
- Validate arguments with scalar specifications.
- Validate argument sequence with collection specification.
- Invoke function
f
. - Validate return value with scalar specification.
- Validate return value with collection specification.
- Validate arguments versus return specifications.
Execution of the returned function will print non-satisfied specs to *out*
and return the value produced by invoking f
with the supplied arguments. At time of wrapping, function metadata must contain any desired specifications, possibly added by inject-specs!. Will not halt on non-valid specs.
Note: Arguments and return value satisfying their specification does not guarantee that the function’s output is correct.