From 90b62bc2dee0c6ca05c6dbeca328d539494f73e2 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Thu, 11 Aug 2022 20:39:33 -0400 Subject: [PATCH 01/12] introduce polymorphic schemas --- README.md | 35 ++++++++ src/clj/schema/macros.clj | 141 +++++++++++++++++++++++++++----- src/cljc/schema/core.cljc | 132 ++++++++++++++++++++++++++++-- src/cljc/schema/spec/core.cljc | 2 +- test/clj/schema/macros_test.clj | 18 +++- test/cljc/schema/core_test.cljc | 100 ++++++++++++++++++++++ 6 files changed, 398 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 7f55707b..582af52f 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,41 @@ You can also write sequence schemas that expect particular values in specific po ;; (not (instance? java.lang.Number "4"))] ``` +### Polymorphic schemas + +Macros such as `s/defn` can define functions with polymorphic schemas. At runtime, they will be checked +by expanding polymorphic variables to their most general values. For example, at runtime `identity-mono` +and `identity-poly` are instrumented in the same way: + +```clojure +(s/defn identity-mono :- s/Any + [x :- s/Any] + x) + +(s/defn :all [T] + identity-poly :- T + [x :- T] + x) +``` + +The actual value chosen as the "most general" depends on the schema variables kind and should not be +relied on. In the future, schema variables may be instantiated with other values. + +Dotted variables have an internal "most general" value which represents a homogeneous sequence of +generalized templates (ie., generalizing variables to the left of the `:..`). +The following two functions are instrumented in the same way. + +```clojure +(s/defn :all [T :..] + rest-args-poly :- T + [& xs :- {:a T} :.. T] + x) + +(s/defn rest-args-mono :- s/Any + [& xs :- [{:a s/Any}]] + x) +``` + ### Other schema types [`schema.core`](https://github.com/plumatic/schema/blob/master/src/cljc/schema/core.cljc) provides many more utilities for building schemas, including `maybe`, `eq`, `enum`, `pred`, `conditional`, `cond-pre`, `constrained`, and more. Here are a few of our favorites: diff --git a/src/clj/schema/macros.clj b/src/clj/schema/macros.clj index 2f65bddd..168077d7 100644 --- a/src/clj/schema/macros.clj +++ b/src/clj/schema/macros.clj @@ -147,12 +147,38 @@ (defn extract-arrow-schematized-element "Take a nonempty seq, which may start like [a ...] or [a :- schema ...], and return a list of [first-element-with-schema-attached rest-elements]" - [env s] - (assert (seq s)) - (let [[f & more] s] - (if (= :- (first more)) - [(normalized-metadata env f (second more)) (drop 2 more)] - [(normalized-metadata env f nil) more]))) + ([env s] (extract-arrow-schematized-element env s false)) + ([env s rest-arg?] + (assert (seq s)) + (let [[f & more] s + [arg more] (if (= :- (first more)) + (let [[arg more] [(normalized-metadata env f (second more)) (drop 2 more)] + [arg more] (if (and rest-arg? (= 2 (count more))) + (if (= :.. (first more)) + (let [dvar (second more)] + (assert! (and (symbol? dvar) + (not (namespace dvar))) + (str "Bad '&' binding: dotted variable must be simple symbol: " (pr-str dvar))) + [(vary-meta arg update :schema + (fn [template] + `(let [template# (fn [~dvar] ~template)] + (cond + (instance? schema.core.AnyDotted ~dvar) + (template# (:schema ~dvar)) + + (vector? ~dvar) + [(apply s/cond-pre ~dvar)] + + :else (throw (ex-info (str ~(format "Unknown value for dotted variable %s: " dvar) + ~dvar) + {})))))) + (drop 2 more)])) + [arg more])] + [arg more]) + [(normalized-metadata env f nil) more])] + (when rest-arg? + (assert! (empty? more) (str "Extra elements after rest argument: " (pr-str more)))) + [arg more]))) (defn process-arrow-schematized-args "Take an arg vector, in which each argument is followed by an optional :- schema, @@ -161,8 +187,11 @@ (loop [in args out []] (if (empty? in) out - (let [[arg more] (extract-arrow-schematized-element env in)] - (recur more (conj out arg)))))) + (if (= '& (first in)) + (let [[arg more] (extract-arrow-schematized-element env (next in) true)] + (recur more (conj out (first in) arg))) + (let [[arg more] (extract-arrow-schematized-element env in)] + (recur more (conj out arg))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -175,7 +204,7 @@ (assert! (or (symbol? rest-arg) (and (vector? rest-arg) (not-any? #{'&} rest-arg))) - "Bad & binding form: currently only bare symbols and vectors supported" (vec post-&)) + "Bad '&' binding form: currently only bare symbols and vectors supported" (vec post-&)) [(vec pre-&) (if (vector? rest-arg) @@ -199,13 +228,14 @@ (if (vector? arg) (simple-arglist-schema-form true arg) [`schema.core/Any]) - (do (assert! (vector? s) "Expected seq schema for rest args, got %s" s) - s)))) + `(let [s# ~s] + (assert! (vector? s#) "Expected seq schema for rest args, got %s" s#) + s#)))) (defn input-schema-form [regular-args rest-arg] (let [base (simple-arglist-schema-form false regular-args)] (if rest-arg - (vec (concat base (rest-arg-schema-form rest-arg))) + `(into ~base ~(rest-arg-schema-form rest-arg)) base))) (defn apply-prepost-conditions @@ -310,12 +340,43 @@ (cons (into regular-args (when rest-arg ['& rest-arg])) body))}))) +(defn parse-poly-binder [binder] + (loop [binder binder + out []] + (if (empty? binder) + out + (let [[sym & binder] binder + _ (assert (and (symbol? sym) + (not (namespace sym))) + (str "Expected simple symbol in polymorphic binder: " sym)) + [opts binder] (loop [binder binder + opts {:kind :schema}] + (if (not (keyword? (first binder))) + [opts binder] + (cond + (= :- (first binder)) (let [[_ kind & binder] binder] + (recur binder (assoc opts :kind kind))) + (= :.. (first binder)) (recur (next binder) (assoc opts :kind :..)) + :else (throw (ex-info (str "Unknown keyword in polymorphic binder: " (first binder)) + {})))))] + (recur binder (conj out [sym opts])))))) + +(defn poly-binder-outer-bindings [binder] + (into [] (mapcat (fn [[sym {:keys [kind]}]] + [sym (case kind + :schema `schema.core/Any + :.. `(schema.core/->AnyDotted schema.core/Any) + (throw (ex-info (str "Unknown kind: " kind) + {})))])) + (parse-poly-binder binder))) + (defn process-fn- "Process the fn args into a final tag proposal, schema form, schema bindings, and fn form" [env name fn-body] (let [compile-validation (compile-fn-validation? env name) output-schema (extract-schema-form name) output-schema-sym (gensym "output-schema") + poly-binder (-> name meta ::binder) bind-meta (or (when-let [t (:tag (meta name))] (when (primitive-sym? t) {:tag t})) @@ -329,6 +390,7 @@ schema-bindings (map :schema-binding processed-arities) fn-forms (map :arity-form processed-arities)] {:outer-bindings (vec (concat + (poly-binder-outer-bindings poly-binder) (when compile-validation `[~(with-meta ufv-sym {:tag 'java.util.concurrent.atomic.AtomicReference}) schema.utils/use-fn-validation]) [output-schema-sym output-schema] @@ -336,9 +398,15 @@ (mapcat :more-bindings processed-arities))) :arglists (map :arglist processed-arities) :raw-arglists (map :raw-arglist processed-arities) - :schema-form (if (= 1 (count processed-arities)) - `(schema.core/->FnSchema ~output-schema-sym ~[(ffirst schema-bindings)]) - `(schema.core/make-fn-schema ~output-schema-sym ~(mapv first schema-bindings))) + :schema-form (if poly-binder + ;; can't reuse output-schema-sym or schema-bindings as type variables are instantiated via poly-binder-outer-bindings + `(schema.core/all ~poly-binder + ~(if (= 1 (count processed-arities)) + `(schema.core/->FnSchema ~output-schema ~[(-> schema-bindings first second)]) + `(schema.core/make-fn-schema ~output-schema ~(mapv second schema-bindings)))) + (if (= 1 (count processed-arities)) + `(schema.core/->FnSchema ~output-schema-sym ~[(ffirst schema-bindings)]) + `(schema.core/make-fn-schema ~output-schema-sym ~(mapv first schema-bindings)))) :fn-body fn-forms})) (defn parse-arity-spec @@ -346,12 +414,34 @@ [spec] (assert! (vector? spec) "An arity spec must be a vector") (let [[init more] ((juxt take-while drop-while) #(not= '& %) spec) - fixed (mapv (fn [i s] `(schema.core/one ~s '~(symbol (str "arg" i)))) (range) init)] + fixed (mapv (fn [i s] `(schema.core/one ~s '~(symbol (str "arg" i)))) (range) init) + start-dotted-idx (count fixed)] (if (empty? more) fixed - (do (assert! (and (= (count more) 2) (vector? (second more))) + (if (= (count more) 4) + (let [[_ template dots dvar] more] + (assert! (and (= :.. dots) + (symbol? dvar) + (not (namespace dvar))) + "An arity with & must be followed by a single sequence schema or dotted variable: %s" + (pr-str more)) + `(into ~fixed (let [dvar# ~dvar + template# (fn [~dvar] ~template)] + (cond + (instance? schema.core.AnyDotted dvar#) + [(template# (:schema dvar#))] + + (vector? dvar#) + (into [] (map-indexed (fn [i# s#] (schema.core/one + (template# s#) + (symbol (str "arg" (+ i# ~start-dotted-idx)))))) + dvar#) + :else (throw (ex-info (str ~(format "Unknown value for dotted variable %s: " dvar) + dvar#) + {})))))) + (do (assert! (and (= (count more) 2) (vector? (second more))) "An arity with & must be followed by a single sequence schema") - (into fixed (second more)))))) + (into fixed (second more))))))) (defn emit-defrecord [defrecord-constructor-sym env name field-schema & more-args] @@ -409,6 +499,17 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Public: helpers for schematized functions +(defn extract-leading-fn-kv-pairs + "Split list into a map of keys allowed at the beginning + of a schema fn macro (eg., :all), and the rest." + [macro-args] + (loop [[k & [v & next-macro-args :as v-provided] :as macro-args] macro-args + leading-opts {}] + (if (= :all k) + (do (assert! v-provided (str "Missing value for key " k)) + (recur next-macro-args (assoc leading-opts k v))) + [leading-opts macro-args]))) + (defn normalized-defn-args "Helper for defining defn-like macros with schemas. Env is &env from the macro body. Reads optional docstring, return type and @@ -416,11 +517,13 @@ returning the normalized arglist. Based on clojure.tools.macro/name-with-attributes." [env macro-args] - (let [[name macro-args] (extract-arrow-schematized-element env macro-args) + (let [[leading-opts macro-args] (extract-leading-fn-kv-pairs macro-args) + [name macro-args] (extract-arrow-schematized-element env macro-args) [maybe-docstring macro-args] (maybe-split-first string? macro-args) [maybe-attr-map macro-args] (maybe-split-first map? macro-args)] (cons (vary-meta name merge (or maybe-attr-map {}) + (when (:all leading-opts) {::binder (:all leading-opts)}) (when maybe-docstring {:doc maybe-docstring})) macro-args))) diff --git a/src/cljc/schema/core.cljc b/src/cljc/schema/core.cljc index 8a074de6..b857544d 100644 --- a/src/cljc/schema/core.cljc +++ b/src/cljc/schema/core.cljc @@ -113,7 +113,7 @@ in a declarative and/or imperative way. See schema.spec.* for examples.") (explain [this] "Expand this schema to a human-readable format suitable for pprinting, - also expanding class schematas at the leaves. Example: + also expanding class schemas at the leaves. Example: user> (s/explain {:a s/Keyword :b [s/Int]} ) {:a Keyword, :b [Int]}")) @@ -1066,6 +1066,68 @@ ([klass schema map-constructor] `(record* ~klass ~schema #(~map-constructor (into {} %)))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Polymorphic Schemas + + +(clojure.core/defrecord ^:no-doc AnyDotted [schema]) + +(declare ^:private inst-most-general) + +(macros/defrecord-schema PolySchema [decl parsed-decl schema-form inst->schema] + Schema + (spec [this] (spec (inst-most-general this))) + (explain [this] (list 'all decl schema-form))) + +(defn- instantiate + "Instantiate a polymorphic schema with schemas." + [{:keys [inst->schema parsed-decl] :as for-all-schema} & schemas] + {:pre [(instance? PolySchema for-all-schema)]} + (macros/assert! (= (count schemas) (count parsed-decl)) + "Wrong number of arguments to instantiate schema %s: expected %s, actual %s" + (explain for-all-schema) + (count parsed-decl) + (count schemas)) + (apply inst->schema schemas)) + +(defn- most-general-insts [=>-schema] + {:pre [(instance? PolySchema =>-schema)]} + (mapv (clojure.core/fn [[sym {:keys [kind]}]] + (case kind + :schema Any + :.. (->AnyDotted Any) + (throw (ex-info (str "Unknown kind: " kind) + {})))) + (:parsed-decl =>-schema))) + +(defn- inst-most-general [=>-schema] + {:pre [(instance? PolySchema =>-schema)]} + (apply instantiate =>-schema (most-general-insts =>-schema))) + +(clojure.core/defn poly-schema? [v] + (instance? PolySchema v)) + +(defmacro all + "Create a polymorphic function schema. + + Binder declaration is a vector of schema variables and its kinds. + + Schema variables have a 'kind' that classify what it represents. + :- assigns a kind to a schema variable. By default, schema variables are kind :schema. + + 1. [T :- :schema] represents a Schema, eg., s/Any, s/Int, (s/=> s/Int s/Bool) + 2. [T :- :..] represents a vector of schemas of kind KIND. + + [T :..] is sugar for [T :- :..]" + [decl schema] + {:pre [(vector? decl)]} + (let [parsed-decl (macros/parse-poly-binder decl)] + `(->PolySchema + '~decl + '~parsed-decl + '~schema + (clojure.core/fn ~(mapv first parsed-decl) ~schema)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Function Schemas @@ -1115,6 +1177,15 @@ each of which is a vector of argument schemas, ending with an optional '& more-schema' specification where more-schema must be a sequence schema. + Dotted schemas may be used as rest schemas, and will be immediately expanded. + + For example, if `Y :..` is in scope then (s/=> Z X & [Y] :.. Y) represents the following functions: + (s/=> Z X) + (s/=> Z X [Y0]) + (s/=> Z X [Y0] [Y1]) + (s/=> Z X [Y0] [Y1] [Y1]) + ...etc + Currently function schemas are purely descriptive; there is no validation except for functions defined directly by s/fn or s/defn" [output-schema & arity-schema-specs] @@ -1240,7 +1311,7 @@ all forms have been executed, resets function validation to its previously set value. Not concurrency-safe." [& body] - `(let [body# (fn [] ~@body)] + `(let [body# (clojure.core/fn [] ~@body)] (if (fn-validation?) (body#) (do @@ -1254,7 +1325,7 @@ all forms have been executed, resets function validation to its previously set value. Not concurrency-safe." [& body] - `(let [body# (fn [] ~@body)] + `(let [body# (clojure.core/fn [] ~@body)] (if (fn-validation?) (do (set-fn-validation! false) @@ -1282,7 +1353,7 @@ [f schema] (vary-meta f assoc :schema schema)) -(clojure.core/defn ^FnSchema fn-schema +(clojure.core/defn fn-schema "Produce the schema for a function defined with s/fn or s/defn." [f] (macros/assert! (fn? f) "Non-function %s" (utils/type-of f)) @@ -1313,10 +1384,12 @@ up to 20 arguments, and then via `apply` beyond 20 arguments. See `cljs.core/with-meta` and `cljs.core.MetaFn`." [& fn-args] - (let [fn-args (if (symbol? (first fn-args)) + (let [[leading-opts fn-args] (macros/extract-leading-fn-kv-pairs fn-args) + fn-args (if (symbol? (first fn-args)) fn-args (cons (gensym "fn") fn-args)) [name more-fn-args] (macros/extract-arrow-schematized-element &env fn-args) + name (vary-meta name merge leading-opts) {:keys [outer-bindings schema-form fn-body]} (macros/process-fn- &env name more-fn-args)] `(let [~@outer-bindings ;; let bind to work around https://clojure.atlassian.net/browse/CLJS-968 @@ -1351,6 +1424,47 @@ See (doc schema.core) for details of the :- syntax for arguments and return schemas. + You can use :all to make a polymorphic schema by binding schema variables. + See `s/all` for more about schema variables. + + The schema variables are scoped inside the function body. Note, they will usually be bound + to their most general values (eg., s/Any) at runtime. This strategy also informs how + `s/with-fn-validation` treats schema variables. However, the values of schema + variables should always be treated as opaque. + + In the body of the function, names provided by argument vectors may shadow schema variables. + + (s/defn :all [T] + my-identity :- T + [x :- T] + ;; from here, (destructured) arguments shadow schema variables + ... + ;; usually equivalent to (s/validate s/Any x) + (s/validate T x)) + + (s/fn-schema my-identity) + ==> (all [T] (=> T T)) + + + Rest parameter schemas may be expanded via dotted variables by placing :.. after + the schema following the '&'. The following example demonstrates most usages of + dotted variables: + + (s/defn :all [X Y :.. Z] + map :- [Z] + [f :- (=> Z X & Y :.. Y) + xs :- [X] + & xss :- [Y] :.. Y] + (apply map f xs xss)) + + The schema for the above function encapsulates the following schemas: + (all [X Z] (=> [Z] (=> Z X) [X])) + (all [X Y0 Z] (=> [Z] (=> Z Y0 X) [X] [Y0])) + (all [X Y0 Y1 Z] (=> [Z] (=> Z Y0 Y1 X) [X] [Y0] [Y1])) + (all [X Y0 Y1 Y2 Z] (=> [Z] (=> Z Y0 Y1 Y2 X) [X] [Y0] [Y1] [Y0])) + ...etc + + The overhead for checking if run-time validation should be used is very small -- about 5% of a very small fn call. On top of that, actual validation costs what it costs. @@ -1387,7 +1501,7 @@ {:keys [outer-bindings schema-form fn-body arglists raw-arglists]} (macros/process-fn- &env name more-defn-args)] `(let ~outer-bindings (let [ret# (clojure.core/defn ~(with-meta name {}) - ~(assoc (apply dissoc standard-meta (when (macros/primitive-sym? tag) [:tag])) + ~(assoc (apply dissoc standard-meta ::macros/binder (when (macros/primitive-sym? tag) [:tag])) :doc (str (str "Inputs: " (if (= 1 (count raw-arglists)) (first raw-arglists) @@ -1442,8 +1556,10 @@ (let [{:keys [outer-bindings fnspecs inner-bindings]} - (reduce (fn [acc fnspec] - (let [[name more-fn-args] (macros/extract-arrow-schematized-element &env fnspec) + (reduce (clojure.core/fn [acc fnspec] + (let [[leading-opts fnspec] (macros/extract-leading-fn-kv-pairs fnspec) + [name more-fn-args] (macros/extract-arrow-schematized-element &env fnspec) + name (vary-meta name merge leading-opts) {:keys [outer-bindings schema-form fn-body]} (macros/process-fn- &env name more-fn-args)] (-> acc (update :outer-bindings into outer-bindings) diff --git a/src/cljc/schema/spec/core.cljc b/src/cljc/schema/spec/core.cljc index f804e64e..cc6e4938 100644 --- a/src/cljc/schema/spec/core.cljc +++ b/src/cljc/schema/spec/core.cljc @@ -11,7 +11,7 @@ (defprotocol CoreSpec "Specs are a common language for Schemas to express their structure. - These two use-cases aren't priveledged, just the two that are considered core + These two use-cases aren't privileged, just the two that are considered core to being a Spec." (subschemas [this] "List all subschemas") diff --git a/test/clj/schema/macros_test.clj b/test/clj/schema/macros_test.clj index bc9f58d7..e80a21dc 100644 --- a/test/clj/schema/macros_test.clj +++ b/test/clj/schema/macros_test.clj @@ -4,8 +4,22 @@ [schema.core :as s] [schema.macros :as macros])) +(deftest extract-leading-fn-kv-pairs-test + (is (= (macros/extract-leading-fn-kv-pairs []) + [{} []])) + (is (= (macros/extract-leading-fn-kv-pairs ['name :- 'schema]) + [{} ['name :- 'schema]])) + (is (= (macros/extract-leading-fn-kv-pairs [:all '[x] 'name :- 'schema]) + [{:all '[x]} ['name :- 'schema]])) + (is (= (macros/extract-leading-fn-kv-pairs [:- '[s/Any] :- 'schema]) + [{} [:- '[s/Any] :- 'schema]])) + (is (= (macros/extract-leading-fn-kv-pairs [:all '[x] :- '[s/Any] :- 'schema]) + [{:all '[x]} [:- '[s/Any] :- 'schema]]))) + (deftest normalized-defn-args-test (doseq [explicit-meta [{} {:a -1 :c 3}] + [leading-map leading-forms] {{} [] + '{::macros/binder [x]} '[:all [x]]} [schema-attrs schema-forms] {{:schema `s/Any} [] {:schema 'Long :tag 'Long} [:- 'Long]} [doc-attrs doc-forms] {{} [] @@ -13,10 +27,10 @@ [attr-map attr-forms] {{} {} {:a 1 :b 2} [{:a 1 :b 2}]}] (let [simple-body ['[x] `(+ 1 1)] - full-args (concat [(with-meta 'abc explicit-meta)] schema-forms doc-forms attr-forms simple-body) + full-args (concat leading-forms [(with-meta 'abc explicit-meta)] schema-forms doc-forms attr-forms simple-body) [name & more] (macros/normalized-defn-args {} full-args)] (testing (vec full-args) - (is (= (concat ['abc (merge explicit-meta schema-attrs doc-attrs attr-map) simple-body]) + (is (= (concat ['abc (merge explicit-meta schema-attrs doc-attrs attr-map leading-map) simple-body]) (concat [name (meta name) more]))))))) (deftest compile-fn-validation?-test diff --git a/test/cljc/schema/core_test.cljc b/test/cljc/schema/core_test.cljc index 6dc2a743..dad9ed9b 100644 --- a/test/cljc/schema/core_test.cljc +++ b/test/cljc/schema/core_test.cljc @@ -697,6 +697,18 @@ (valid! schema {}) (is (= '(=>* Keyword [Int] [Int & [Keyword]]) (s/explain schema))))) +(deftest dotted-fn-schema-test + (testing "expand dotted template" + (let [X [s/Int s/Bool] + schema (s/=> s/Keyword s/Int s/Int & [X] :.. X)] + (is (= (s/=> s/Keyword s/Int s/Int [s/Int] [s/Bool]) + schema)))) + (testing "expand AnyDotted" + (let [X (s/->AnyDotted s/Int) + schema (s/=> s/Keyword s/Int s/Int & [X] :.. X)] + (is (= (s/=> s/Keyword s/Int s/Int & [[s/Int]]) + schema))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Schematized defrecord @@ -1575,3 +1587,91 @@ (catch Exception e (is (re-find #"ef408750" (#?(:cljs .-message :clj .getMessage) e))))))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Polymorphic schemas + +(s/defn :all [x] + args-shadow-schema-variables :- x + [x :- x] + x) + +(s/defn :all [T] + poly-identity :- T + [x :- T] + (s/validate T x)) + +(s/defn :all [T] + poly-first :- T + [xs :- [T]] + (first xs)) + +#_ ;;TODO +(s/defn :all [T ;TODO :- [:=> :schema :schema :schema] + ] + ho-schema-fn :- T + [xs :- (T s/Int s/Bool)] + (first xs)) + +(s/defn :all [X Y] + poly-map-nodot :- [Y] + [f :- (s/=> Y X) + xs :- [X]] + (map f xs)) + +(s/defn :all [X Y :.. Z] + poly-map-dot :- [Z] + [f :- (s/=> Z X & Y :.. Y) + xs :- [X] + & xss :- [Y] :.. Y] + (apply map f xs xss)) + +(deftest explain-all-test + (is (= '(all [x] (s/=> x)) (s/explain (s/all [x] (s/=> x))))) + ;;FIXME ideally (all [T] (s/=> T)) + (is (= '(all [T] (schema.core/->FnSchema T [[(schema.core/one T (quote x))]])) + (s/explain (s/fn-schema poly-identity))))) + +(deftest inst-test + (is (= (@#'s/instantiate (s/all [a] (s/=> a)) + s/Int) + (s/=> s/Int))) + (is (not= (@#'s/instantiate (s/all [a] (s/=> a)) + s/Bool) + (s/=> s/Int))) + (is (= (@#'s/instantiate (s/all [a] (s/=> a a)) + s/Int) + (s/=> s/Int s/Int))) + (is (= (@#'s/instantiate (s/all [a b] (s/=> a b a b)) + s/Int s/Bool) + (s/=> s/Int s/Bool s/Int s/Bool))) + (is (= '(=> Int Int) + (s/explain (@#'s/instantiate (s/fn-schema poly-identity) s/Int)))) + (is (thrown-with-msg? Exception #"Wrong number of arguments" + (@#'s/instantiate (s/fn-schema poly-map-nodot) s/Int)))) + +(deftest poly-defn-semantics-test + (is (= 1 (s/with-fn-validation (args-shadow-schema-variables 1)))) + (is (= 1 (s/with-fn-validation (poly-identity 1)))) + (is (= :a (s/with-fn-validation (poly-identity :a)))) + (s/with-fn-validation (invalid-call! poly-first 1)) + (is (= 1 (s/with-fn-validation (poly-first [1])))) + (is (= 1 (s/with-fn-validation (poly-first [1])))) + (is (= [2 3] (s/with-fn-validation (poly-map-nodot inc [1 2])))) + (s/with-fn-validation (invalid-call! poly-map-nodot 1 2)) + (is (= [2 3] (s/with-fn-validation (poly-map-dot inc [1 2])))) + (is (= [3 5] (s/with-fn-validation (poly-map-dot + [1 2] [2 3])))) + (s/with-fn-validation (invalid-call! poly-map-dot 1 2))) + +(deftest inst-most-general-test + (is (= '(=> Any Any) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-identity))))) + (is (= '(=> Any [Any]) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-first))))) + (is (= '(=> [Any] (=> Any Any) [Any]) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-nodot))))) + (is (= '(=> [Any] (=> Any Any & [Any]) [Any] & [Any]) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-dot)))))) + +;; TODO test s/defn, s/fn, s/letfn From 947ce5065be9aaabc9ff2b3ae3f3dbf934f20f37 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 10:38:48 -0400 Subject: [PATCH 02/12] change (=> B & A :.. A) to (=> B A :.. A) --- README.md | 8 ++--- src/clj/schema/macros.clj | 59 ++++++++++++++++++--------------- src/cljc/schema/core.cljc | 4 +-- test/cljc/schema/core_test.cljc | 6 ++-- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 582af52f..82aa1128 100644 --- a/README.md +++ b/README.md @@ -315,13 +315,13 @@ generalized templates (ie., generalizing variables to the left of the `:..`). The following two functions are instrumented in the same way. ```clojure -(s/defn :all [T :..] - rest-args-poly :- T - [& xs :- {:a T} :.. T] +(s/defn :all [S T :..] + rest-args-poly :- S + [& xs :- {:a S :b T} :.. T] x) (s/defn rest-args-mono :- s/Any - [& xs :- [{:a s/Any}]] + [& xs :- [{:a s/Any :b s/Any}]] x) ``` diff --git a/src/clj/schema/macros.clj b/src/clj/schema/macros.clj index 168077d7..118db415 100644 --- a/src/clj/schema/macros.clj +++ b/src/clj/schema/macros.clj @@ -414,34 +414,41 @@ [spec] (assert! (vector? spec) "An arity spec must be a vector") (let [[init more] ((juxt take-while drop-while) #(not= '& %) spec) + init (vec init) + rest-schema? (= '& (first more)) + dotted-schema? (and (<= 3 (count init)) + (= :.. (-> init pop peek))) + _ (assert! (not (and rest-schema? dotted-schema?)) + "Cannot provide both & and :.. to =>.") + [init template dvar] (if dotted-schema? + [(-> init pop pop pop) (-> init pop pop peek) (-> init peek)] + [init]) + _ (when dotted-schema? + (assert! (and (symbol? dvar) + (not (namespace dvar))) + "Dotted variable after :.. must be a simple symbol: %s" + (pr-str dvar))) fixed (mapv (fn [i s] `(schema.core/one ~s '~(symbol (str "arg" i)))) (range) init) start-dotted-idx (count fixed)] - (if (empty? more) - fixed - (if (= (count more) 4) - (let [[_ template dots dvar] more] - (assert! (and (= :.. dots) - (symbol? dvar) - (not (namespace dvar))) - "An arity with & must be followed by a single sequence schema or dotted variable: %s" - (pr-str more)) - `(into ~fixed (let [dvar# ~dvar - template# (fn [~dvar] ~template)] - (cond - (instance? schema.core.AnyDotted dvar#) - [(template# (:schema dvar#))] - - (vector? dvar#) - (into [] (map-indexed (fn [i# s#] (schema.core/one - (template# s#) - (symbol (str "arg" (+ i# ~start-dotted-idx)))))) - dvar#) - :else (throw (ex-info (str ~(format "Unknown value for dotted variable %s: " dvar) - dvar#) - {})))))) - (do (assert! (and (= (count more) 2) (vector? (second more))) - "An arity with & must be followed by a single sequence schema") - (into fixed (second more))))))) + (cond + rest-schema? (do (assert! (and (= (count more) 2) (vector? (second more))) + "An arity with & must be followed by a single sequence schema") + (into fixed (second more))) + dotted-schema? `(into ~fixed (let [dvar# ~dvar + template# (fn [~dvar] ~template)] + (cond + (instance? schema.core.AnyDotted dvar#) + [(template# (:schema dvar#))] + + (vector? dvar#) + (into [] (map-indexed (fn [i# s#] (schema.core/one + (template# s#) + (symbol (str "arg" (+ i# ~start-dotted-idx)))))) + dvar#) + :else (throw (ex-info (str ~(format "Unknown value for dotted variable %s: " dvar) + dvar#) + {}))))) + :else fixed))) (defn emit-defrecord [defrecord-constructor-sym env name field-schema & more-args] diff --git a/src/cljc/schema/core.cljc b/src/cljc/schema/core.cljc index b857544d..0a747162 100644 --- a/src/cljc/schema/core.cljc +++ b/src/cljc/schema/core.cljc @@ -1179,7 +1179,7 @@ Dotted schemas may be used as rest schemas, and will be immediately expanded. - For example, if `Y :..` is in scope then (s/=> Z X & [Y] :.. Y) represents the following functions: + For example, if `Y :..` is in scope then (s/=> Z X [Y] :.. Y) represents the following functions: (s/=> Z X) (s/=> Z X [Y0]) (s/=> Z X [Y0] [Y1]) @@ -1452,7 +1452,7 @@ (s/defn :all [X Y :.. Z] map :- [Z] - [f :- (=> Z X & Y :.. Y) + [f :- (=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss)) diff --git a/test/cljc/schema/core_test.cljc b/test/cljc/schema/core_test.cljc index dad9ed9b..aa27773f 100644 --- a/test/cljc/schema/core_test.cljc +++ b/test/cljc/schema/core_test.cljc @@ -700,12 +700,12 @@ (deftest dotted-fn-schema-test (testing "expand dotted template" (let [X [s/Int s/Bool] - schema (s/=> s/Keyword s/Int s/Int & [X] :.. X)] + schema (s/=> s/Keyword s/Int s/Int [X] :.. X)] (is (= (s/=> s/Keyword s/Int s/Int [s/Int] [s/Bool]) schema)))) (testing "expand AnyDotted" (let [X (s/->AnyDotted s/Int) - schema (s/=> s/Keyword s/Int s/Int & [X] :.. X)] + schema (s/=> s/Keyword s/Int s/Int [X] :.. X)] (is (= (s/=> s/Keyword s/Int s/Int & [[s/Int]]) schema))))) @@ -1622,7 +1622,7 @@ (s/defn :all [X Y :.. Z] poly-map-dot :- [Z] - [f :- (s/=> Z X & Y :.. Y) + [f :- (s/=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss)) From b13d67f9a655fda11d70b61c7c85447acbbf05ba Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 10:40:23 -0400 Subject: [PATCH 03/12] doc: schema variable => polymorphic variable --- README.md | 4 ++-- src/cljc/schema/core.cljc | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 82aa1128..b7eed17c 100644 --- a/README.md +++ b/README.md @@ -307,8 +307,8 @@ and `identity-poly` are instrumented in the same way: x) ``` -The actual value chosen as the "most general" depends on the schema variables kind and should not be -relied on. In the future, schema variables may be instantiated with other values. +The actual value chosen as the "most general" depends on the polymorphic variables kind and should not be +relied on. In the future, polymorphic variables may be instantiated with other values. Dotted variables have an internal "most general" value which represents a homogeneous sequence of generalized templates (ie., generalizing variables to the left of the `:..`). diff --git a/src/cljc/schema/core.cljc b/src/cljc/schema/core.cljc index 0a747162..fab963fd 100644 --- a/src/cljc/schema/core.cljc +++ b/src/cljc/schema/core.cljc @@ -1110,10 +1110,10 @@ (defmacro all "Create a polymorphic function schema. - Binder declaration is a vector of schema variables and its kinds. + Binder declaration is a vector of polymorphic variables and its kinds. Schema variables have a 'kind' that classify what it represents. - :- assigns a kind to a schema variable. By default, schema variables are kind :schema. + :- assigns a kind to a polymorphic variable. By default, polymorphic variables are kind :schema. 1. [T :- :schema] represents a Schema, eg., s/Any, s/Int, (s/=> s/Int s/Bool) 2. [T :- :..] represents a vector of schemas of kind KIND. @@ -1424,20 +1424,20 @@ See (doc schema.core) for details of the :- syntax for arguments and return schemas. - You can use :all to make a polymorphic schema by binding schema variables. - See `s/all` for more about schema variables. + You can use :all to make a polymorphic schema by binding polymorphic variables. + See `s/all` for more about polymorphic variables. - The schema variables are scoped inside the function body. Note, they will usually be bound + The polymorphic variables are scoped inside the function body. Note, they will usually be bound to their most general values (eg., s/Any) at runtime. This strategy also informs how - `s/with-fn-validation` treats schema variables. However, the values of schema + `s/with-fn-validation` treats polymorphic variables. However, the values of schema variables should always be treated as opaque. - In the body of the function, names provided by argument vectors may shadow schema variables. + In the body of the function, names provided by argument vectors may shadow polymorphic variables. (s/defn :all [T] my-identity :- T [x :- T] - ;; from here, (destructured) arguments shadow schema variables + ;; from here, (destructured) arguments shadow polymorphic variables ... ;; usually equivalent to (s/validate s/Any x) (s/validate T x)) From 575fa0702d1e065b8b3a7a2783717169abd6c89b Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 10:50:13 -0400 Subject: [PATCH 04/12] fix doc --- src/cljc/schema/core.cljc | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/cljc/schema/core.cljc b/src/cljc/schema/core.cljc index fab963fd..ccea6fcb 100644 --- a/src/cljc/schema/core.cljc +++ b/src/cljc/schema/core.cljc @@ -1116,7 +1116,8 @@ :- assigns a kind to a polymorphic variable. By default, polymorphic variables are kind :schema. 1. [T :- :schema] represents a Schema, eg., s/Any, s/Int, (s/=> s/Int s/Bool) - 2. [T :- :..] represents a vector of schemas of kind KIND. + 2. [T :- :..] represents a vector of schemas, often to represent heterogenous rest arguments + eg., [s/Int s/Bool] [T :..] is sugar for [T :- :..]" [decl schema] @@ -1179,13 +1180,17 @@ Dotted schemas may be used as rest schemas, and will be immediately expanded. - For example, if `Y :..` is in scope then (s/=> Z X [Y] :.. Y) represents the following functions: - (s/=> Z X) - (s/=> Z X [Y0]) - (s/=> Z X [Y0] [Y1]) - (s/=> Z X [Y0] [Y1] [Y1]) + For example, if `Y :..` is in scope then (=> Z X [Y] :.. Y) represents the following functions: + (=> Z X) + (=> Z X [Y0]) + (=> Z X [Y0] [Y1]) + (=> Z X [Y0] [Y1] [Y1]) ...etc + Depending on the instantiation of Y, the schema at runtime will be one of the above or + the following schema that encapsulates them all: + (=> Z X & [s/Any]) + Currently function schemas are purely descriptive; there is no validation except for functions defined directly by s/fn or s/defn" [output-schema & arity-schema-specs] From e7a1a6c8306f3d77dee416b83b189a5e8511aa33 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 10:52:10 -0400 Subject: [PATCH 05/12] fix doc --- src/cljc/schema/core.cljc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cljc/schema/core.cljc b/src/cljc/schema/core.cljc index ccea6fcb..2cb0fff3 100644 --- a/src/cljc/schema/core.cljc +++ b/src/cljc/schema/core.cljc @@ -1187,9 +1187,9 @@ (=> Z X [Y0] [Y1] [Y1]) ...etc - Depending on the instantiation of Y, the schema at runtime will be one of the above or - the following schema that encapsulates them all: - (=> Z X & [s/Any]) + Depending on the instantiation of Y, the schema at runtime will be one of the above with + concretized Y's or the following schema that encapsulates them all: + (=> Z X & [[s/Any]]) Currently function schemas are purely descriptive; there is no validation except for functions defined directly by s/fn or s/defn" From f73ec995faf95f69907aabee5526912fb7d21801 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 11:10:30 -0400 Subject: [PATCH 06/12] support poly s/fn --- src/clj/schema/macros.clj | 6 +-- test/clj/schema/macros_test.clj | 6 +-- test/cljc/schema/core_test.cljc | 70 +++++++++++++++++++++++++++------ 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/clj/schema/macros.clj b/src/clj/schema/macros.clj index 118db415..e1d1c6bf 100644 --- a/src/clj/schema/macros.clj +++ b/src/clj/schema/macros.clj @@ -376,7 +376,7 @@ (let [compile-validation (compile-fn-validation? env name) output-schema (extract-schema-form name) output-schema-sym (gensym "output-schema") - poly-binder (-> name meta ::binder) + poly-binder (-> name meta ::poly-binder) bind-meta (or (when-let [t (:tag (meta name))] (when (primitive-sym? t) {:tag t})) @@ -514,7 +514,7 @@ leading-opts {}] (if (= :all k) (do (assert! v-provided (str "Missing value for key " k)) - (recur next-macro-args (assoc leading-opts k v))) + (recur next-macro-args (assoc leading-opts ::poly-binder v))) [leading-opts macro-args]))) (defn normalized-defn-args @@ -530,7 +530,7 @@ [maybe-attr-map macro-args] (maybe-split-first map? macro-args)] (cons (vary-meta name merge (or maybe-attr-map {}) - (when (:all leading-opts) {::binder (:all leading-opts)}) + leading-opts (when maybe-docstring {:doc maybe-docstring})) macro-args))) diff --git a/test/clj/schema/macros_test.clj b/test/clj/schema/macros_test.clj index e80a21dc..acbe1037 100644 --- a/test/clj/schema/macros_test.clj +++ b/test/clj/schema/macros_test.clj @@ -10,16 +10,16 @@ (is (= (macros/extract-leading-fn-kv-pairs ['name :- 'schema]) [{} ['name :- 'schema]])) (is (= (macros/extract-leading-fn-kv-pairs [:all '[x] 'name :- 'schema]) - [{:all '[x]} ['name :- 'schema]])) + [{::macros/poly-binder '[x]} ['name :- 'schema]])) (is (= (macros/extract-leading-fn-kv-pairs [:- '[s/Any] :- 'schema]) [{} [:- '[s/Any] :- 'schema]])) (is (= (macros/extract-leading-fn-kv-pairs [:all '[x] :- '[s/Any] :- 'schema]) - [{:all '[x]} [:- '[s/Any] :- 'schema]]))) + [{::macros/poly-binder '[x]} [:- '[s/Any] :- 'schema]]))) (deftest normalized-defn-args-test (doseq [explicit-meta [{} {:a -1 :c 3}] [leading-map leading-forms] {{} [] - '{::macros/binder [x]} '[:all [x]]} + '{::macros/poly-binder [x]} '[:all [x]]} [schema-attrs schema-forms] {{:schema `s/Any} [] {:schema 'Long :tag 'Long} [:- 'Long]} [doc-attrs doc-forms] {{} [] diff --git a/test/cljc/schema/core_test.cljc b/test/cljc/schema/core_test.cljc index aa27773f..7745ee6b 100644 --- a/test/cljc/schema/core_test.cljc +++ b/test/cljc/schema/core_test.cljc @@ -1651,6 +1651,16 @@ (is (thrown-with-msg? Exception #"Wrong number of arguments" (@#'s/instantiate (s/fn-schema poly-map-nodot) s/Int)))) +(deftest inst-most-general-test + (is (= '(=> Any Any) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-identity))))) + (is (= '(=> Any [Any]) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-first))))) + (is (= '(=> [Any] (=> Any Any) [Any]) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-nodot))))) + (is (= '(=> [Any] (=> Any Any & [Any]) [Any] & [Any]) + (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-dot)))))) + (deftest poly-defn-semantics-test (is (= 1 (s/with-fn-validation (args-shadow-schema-variables 1)))) (is (= 1 (s/with-fn-validation (poly-identity 1)))) @@ -1664,14 +1674,52 @@ (is (= [3 5] (s/with-fn-validation (poly-map-dot + [1 2] [2 3])))) (s/with-fn-validation (invalid-call! poly-map-dot 1 2))) -(deftest inst-most-general-test - (is (= '(=> Any Any) - (s/explain (@#'s/inst-most-general (s/fn-schema poly-identity))))) - (is (= '(=> Any [Any]) - (s/explain (@#'s/inst-most-general (s/fn-schema poly-first))))) - (is (= '(=> [Any] (=> Any Any) [Any]) - (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-nodot))))) - (is (= '(=> [Any] (=> Any Any & [Any]) [Any] & [Any]) - (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-dot)))))) - -;; TODO test s/defn, s/fn, s/letfn +(def sfn-args-shadow-schema-variables + (s/fn :all [x] + args-shadow-schema-variables :- x + [x :- x] + x)) + +(def sfn-poly-identity + (s/fn :all [T] + poly-identity :- T + [x :- T] + (s/validate T x))) + +(def sfn-poly-first + (s/fn :all [T] + poly-first :- T + [xs :- [T]] + (first xs))) + +(def sfn-poly-map-nodot + (s/fn :all [X Y] + poly-map-nodot :- [Y] + [f :- (s/=> Y X) + xs :- [X]] + (map f xs))) + +(def sfn-poly-map-dot + (s/fn :all [X Y :.. Z] + poly-map-dot :- [Z] + [f :- (s/=> Z X Y :.. Y) + xs :- [X] + & xss :- [Y] :.. Y] + (apply map f xs xss))) + +(deftest poly-fn-semantics-test + (testing "no name" + (is (= 1 ((s/fn :all [T] [x :- T] x) 1)))) + (is (= 1 (s/with-fn-validation (sfn-args-shadow-schema-variables 1)))) + (is (= 1 (s/with-fn-validation (sfn-poly-identity 1)))) + (is (= :a (s/with-fn-validation (sfn-poly-identity :a)))) + (s/with-fn-validation (invalid-call! sfn-poly-first 1)) + (is (= 1 (s/with-fn-validation (sfn-poly-first [1])))) + (is (= 1 (s/with-fn-validation (sfn-poly-first [1])))) + (is (= [2 3] (s/with-fn-validation (sfn-poly-map-nodot inc [1 2])))) + (s/with-fn-validation (invalid-call! sfn-poly-map-nodot 1 2)) + (is (= [2 3] (s/with-fn-validation (sfn-poly-map-dot inc [1 2])))) + (is (= [3 5] (s/with-fn-validation (sfn-poly-map-dot + [1 2] [2 3])))) + (s/with-fn-validation (invalid-call! sfn-poly-map-dot 1 2))) + +;; TODO test s/fn, s/letfn From 1673cf8e6bfb76dbac785150b059f497185167ff Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 11:22:18 -0400 Subject: [PATCH 07/12] test s/letfn --- test/cljc/schema/core_test.cljc | 116 ++++++++++++++++---------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/test/cljc/schema/core_test.cljc b/test/cljc/schema/core_test.cljc index 7745ee6b..63779497 100644 --- a/test/cljc/schema/core_test.cljc +++ b/test/cljc/schema/core_test.cljc @@ -1592,6 +1592,31 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Polymorphic schemas +(s/defschema PolySemanticsTestSuite + {:args-shadow-schema-variables (s/all [x] (s/=> x x)) + :poly-identity (s/all [T] (s/=> T T)) + :poly-first (s/all [T] (s/=> T [T])) + :poly-map-nodot (s/all [X Y] (s/=> [Y] (s/=> Y X) [X])) + :poly-map-dot (s/all [X Y :.. Z] (s/=> [Z] (s/=> Z X Y :.. Y) [X] [Y] :.. Y))}) + +(s/defn ^:always-validate poly-semantics-test-suite + [{:keys [args-shadow-schema-variables + poly-identity + poly-first + poly-map-nodot + poly-map-dot]} :- PolySemanticsTestSuite] + (is (= 1 (s/with-fn-validation (args-shadow-schema-variables 1)))) + (is (= 1 (s/with-fn-validation (poly-identity 1)))) + (is (= :a (s/with-fn-validation (poly-identity :a)))) + (s/with-fn-validation (invalid-call! poly-first 1)) + (is (= 1 (s/with-fn-validation (poly-first [1])))) + (is (= 1 (s/with-fn-validation (poly-first [1])))) + (is (= [2 3] (s/with-fn-validation (poly-map-nodot inc [1 2])))) + (s/with-fn-validation (invalid-call! poly-map-nodot 1 2)) + (is (= [2 3] (s/with-fn-validation (poly-map-dot inc [1 2])))) + (is (= [3 5] (s/with-fn-validation (poly-map-dot + [1 2] [2 3])))) + (s/with-fn-validation (invalid-call! poly-map-dot 1 2))) + (s/defn :all [x] args-shadow-schema-variables :- x [x :- x] @@ -1662,64 +1687,39 @@ (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-dot)))))) (deftest poly-defn-semantics-test - (is (= 1 (s/with-fn-validation (args-shadow-schema-variables 1)))) - (is (= 1 (s/with-fn-validation (poly-identity 1)))) - (is (= :a (s/with-fn-validation (poly-identity :a)))) - (s/with-fn-validation (invalid-call! poly-first 1)) - (is (= 1 (s/with-fn-validation (poly-first [1])))) - (is (= 1 (s/with-fn-validation (poly-first [1])))) - (is (= [2 3] (s/with-fn-validation (poly-map-nodot inc [1 2])))) - (s/with-fn-validation (invalid-call! poly-map-nodot 1 2)) - (is (= [2 3] (s/with-fn-validation (poly-map-dot inc [1 2])))) - (is (= [3 5] (s/with-fn-validation (poly-map-dot + [1 2] [2 3])))) - (s/with-fn-validation (invalid-call! poly-map-dot 1 2))) - -(def sfn-args-shadow-schema-variables - (s/fn :all [x] - args-shadow-schema-variables :- x - [x :- x] - x)) - -(def sfn-poly-identity - (s/fn :all [T] - poly-identity :- T - [x :- T] - (s/validate T x))) - -(def sfn-poly-first - (s/fn :all [T] - poly-first :- T - [xs :- [T]] - (first xs))) - -(def sfn-poly-map-nodot - (s/fn :all [X Y] - poly-map-nodot :- [Y] - [f :- (s/=> Y X) - xs :- [X]] - (map f xs))) - -(def sfn-poly-map-dot - (s/fn :all [X Y :.. Z] - poly-map-dot :- [Z] - [f :- (s/=> Z X Y :.. Y) - xs :- [X] - & xss :- [Y] :.. Y] - (apply map f xs xss))) + (poly-semantics-test-suite + {:args-shadow-schema-variables args-shadow-schema-variables + :poly-identity poly-identity + :poly-first poly-first + :poly-map-nodot poly-map-nodot + :poly-map-dot poly-map-dot})) (deftest poly-fn-semantics-test (testing "no name" - (is (= 1 ((s/fn :all [T] [x :- T] x) 1)))) - (is (= 1 (s/with-fn-validation (sfn-args-shadow-schema-variables 1)))) - (is (= 1 (s/with-fn-validation (sfn-poly-identity 1)))) - (is (= :a (s/with-fn-validation (sfn-poly-identity :a)))) - (s/with-fn-validation (invalid-call! sfn-poly-first 1)) - (is (= 1 (s/with-fn-validation (sfn-poly-first [1])))) - (is (= 1 (s/with-fn-validation (sfn-poly-first [1])))) - (is (= [2 3] (s/with-fn-validation (sfn-poly-map-nodot inc [1 2])))) - (s/with-fn-validation (invalid-call! sfn-poly-map-nodot 1 2)) - (is (= [2 3] (s/with-fn-validation (sfn-poly-map-dot inc [1 2])))) - (is (= [3 5] (s/with-fn-validation (sfn-poly-map-dot + [1 2] [2 3])))) - (s/with-fn-validation (invalid-call! sfn-poly-map-dot 1 2))) - -;; TODO test s/fn, s/letfn + (is (= 1 ((s/fn :all [T] [x :- T] x) 1))) + (poly-semantics-test-suite + {:args-shadow-schema-variables (s/fn :all [x] :- x [x :- x] x) + :poly-identity (s/fn :all [T] :- T [x :- T] (s/validate T x)) + :poly-first (s/fn :all [T] :- T [xs :- [T]] (first xs)) + :poly-map-nodot (s/fn :all [X Y] :- [Y] [f :- (s/=> Y X) xs :- [X]] (map f xs)) + :poly-map-dot (s/fn :all [X Y :.. Z] :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss))})) + (testing "with name" + (poly-semantics-test-suite + {:args-shadow-schema-variables (s/fn :all [x] args-shadow-schema-variables :- x [x :- x] x) + :poly-identity (s/fn :all [T] poly-identity :- T [x :- T] (s/validate T x)) + :poly-first (s/fn :all [T] poly-first :- T [xs :- [T]] (first xs)) + :poly-map-nodot (s/fn :all [X Y] poly-map-nodot :- [Y] [f :- (s/=> Y X) xs :- [X]] (map f xs)) + :poly-map-dot (s/fn :all [X Y :.. Z] poly-map-dot :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss))}))) + +(deftest poly-letfn-semantics-test + (poly-semantics-test-suite + {:args-shadow-schema-variables (s/letfn [(:all [x] args-shadow-schema-variables :- x [x :- x] x)] + args-shadow-schema-variables) + :poly-identity (s/letfn [(:all [T] poly-identity :- T [x :- T] (s/validate T x))] + poly-identity) + :poly-first (s/letfn [(:all [T] poly-first :- T [xs :- [T]] (first xs))] + poly-first) + :poly-map-nodot (s/letfn [(:all [X Y] poly-map-nodot :- [Y] [f :- (s/=> Y X) xs :- [X]] (map f xs))] + poly-map-nodot) + :poly-map-dot (s/letfn [(:all [X Y :.. Z] poly-map-dot :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss))] + poly-map-dot)})) From 47b2c8a502b3edb0b9ddac28e631cfce9a95d0a5 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 11:46:02 -0400 Subject: [PATCH 08/12] fix s/defn dotted expanding to rest --- src/clj/schema/macros.clj | 2 +- test/cljc/schema/core_test.cljc | 81 +++++++++++++++++++++++++++++---- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/src/clj/schema/macros.clj b/src/clj/schema/macros.clj index e1d1c6bf..1b5770f0 100644 --- a/src/clj/schema/macros.clj +++ b/src/clj/schema/macros.clj @@ -164,7 +164,7 @@ `(let [template# (fn [~dvar] ~template)] (cond (instance? schema.core.AnyDotted ~dvar) - (template# (:schema ~dvar)) + [(template# (:schema ~dvar))] (vector? ~dvar) [(apply s/cond-pre ~dvar)] diff --git a/test/cljc/schema/core_test.cljc b/test/cljc/schema/core_test.cljc index 63779497..118411ad 100644 --- a/test/cljc/schema/core_test.cljc +++ b/test/cljc/schema/core_test.cljc @@ -1597,7 +1597,13 @@ :poly-identity (s/all [T] (s/=> T T)) :poly-first (s/all [T] (s/=> T [T])) :poly-map-nodot (s/all [X Y] (s/=> [Y] (s/=> Y X) [X])) - :poly-map-dot (s/all [X Y :.. Z] (s/=> [Z] (s/=> Z X Y :.. Y) [X] [Y] :.. Y))}) + :poly-map-dot (s/all [X Y :.. Z] (s/=> [Z] (s/=> Z X Y :.. Y) [X] [Y] :.. Y)) + :poly-map-dot-arities (s/all [X Y Z S :.. T] + (s/=>* [T] + [(s/=> T X) [X]] + [(s/=> T X Y) [X] [Y]] + [(s/=> T X Y Z) [X] [Y] [Z]] + [(s/=> T X Y Z S :.. S) [X] [Y] [Z] [S] :.. S]))}) (s/defn ^:always-validate poly-semantics-test-suite [{:keys [args-shadow-schema-variables @@ -1615,7 +1621,21 @@ (s/with-fn-validation (invalid-call! poly-map-nodot 1 2)) (is (= [2 3] (s/with-fn-validation (poly-map-dot inc [1 2])))) (is (= [3 5] (s/with-fn-validation (poly-map-dot + [1 2] [2 3])))) - (s/with-fn-validation (invalid-call! poly-map-dot 1 2))) + (s/with-fn-validation (invalid-call! poly-map-dot 1 2)) + (s/with-fn-validation (invalid-call! poly-map-dot + 1)) + (s/with-fn-validation (invalid-call! poly-map-dot + [1] 2)) + (is (= [2 3] (s/with-fn-validation (poly-map-dot-arities inc [1 2])))) + (is (= [3 5] (s/with-fn-validation (poly-map-dot-arities + [1 2] [2 3])))) + (is (= [7 10] (s/with-fn-validation (poly-map-dot-arities + [1 2] [2 3] [4 5])))) + (is (= [13 17] (s/with-fn-validation (poly-map-dot-arities + [1 2] [2 3] [4 5] [6 7])))) + (is (= [21 26] (s/with-fn-validation (poly-map-dot-arities + [1 2] [2 3] [4 5] [6 7] [8 9])))) + (s/with-fn-validation (invalid-call! poly-map-dot-arities 1 1)) + (s/with-fn-validation (invalid-call! poly-map-dot-arities + 1)) + (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] 2)) + (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] [2] 3)) + (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] [2] [3] 4)) + (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] [2] [3] [4] 5)) +) (s/defn :all [x] args-shadow-schema-variables :- x @@ -1649,8 +1669,29 @@ poly-map-dot :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] - & xss :- [Y] :.. Y] - (apply map f xs xss)) + & yss :- [Y] :.. Y] + (apply map f xs yss)) + +(s/defn :all [X Y Z S :.. T] + poly-map-dot-arities :- [T] + ([f :- (s/=> T X) + xs :- [X]] + (map f xs)) + ([f :- (s/=> T X Y) + xs :- [X] + ys :- [Y]] + (map f xs ys)) + ([f :- (s/=> T X Y Z) + xs :- [X] + ys :- [Y] + zs :- [Z]] + (map f xs ys zs)) + ([f :- (s/=> T X Y Z S :.. S) + xs :- [X] + ys :- [Y] + zs :- [Z] + & ss :- [S] :.. S] + (apply map f xs ys zs ss))) (deftest explain-all-test (is (= '(all [x] (s/=> x)) (s/explain (s/all [x] (s/=> x))))) @@ -1683,7 +1724,7 @@ (s/explain (@#'s/inst-most-general (s/fn-schema poly-first))))) (is (= '(=> [Any] (=> Any Any) [Any]) (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-nodot))))) - (is (= '(=> [Any] (=> Any Any & [Any]) [Any] & [Any]) + (is (= '(=> [Any] (=> Any Any & [Any]) [Any] & [[Any]]) (s/explain (@#'s/inst-most-general (s/fn-schema poly-map-dot)))))) (deftest poly-defn-semantics-test @@ -1692,7 +1733,8 @@ :poly-identity poly-identity :poly-first poly-first :poly-map-nodot poly-map-nodot - :poly-map-dot poly-map-dot})) + :poly-map-dot poly-map-dot + :poly-map-dot-arities poly-map-dot-arities})) (deftest poly-fn-semantics-test (testing "no name" @@ -1702,14 +1744,26 @@ :poly-identity (s/fn :all [T] :- T [x :- T] (s/validate T x)) :poly-first (s/fn :all [T] :- T [xs :- [T]] (first xs)) :poly-map-nodot (s/fn :all [X Y] :- [Y] [f :- (s/=> Y X) xs :- [X]] (map f xs)) - :poly-map-dot (s/fn :all [X Y :.. Z] :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss))})) + :poly-map-dot (s/fn :all [X Y :.. Z] :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & yss :- [Y] :.. Y] (apply map f xs yss)) + :poly-map-dot-arities (s/fn :all [X Y Z S :.. T] + :- [T] + ([f :- (s/=> T X) xs :- [X]] (map f xs)) + ([f :- (s/=> T X Y) xs :- [X] ys :- [Y]] (map f xs ys)) + ([f :- (s/=> T X Y Z) xs :- [X] ys :- [Y] zs :- [Z]] (map f xs ys zs)) + ([f :- (s/=> T X Y Z S :.. S) xs :- [X] ys :- [Y] zs :- [Z] & ss :- [S] :.. S] (apply map f xs ys zs ss)))})) (testing "with name" (poly-semantics-test-suite {:args-shadow-schema-variables (s/fn :all [x] args-shadow-schema-variables :- x [x :- x] x) :poly-identity (s/fn :all [T] poly-identity :- T [x :- T] (s/validate T x)) :poly-first (s/fn :all [T] poly-first :- T [xs :- [T]] (first xs)) :poly-map-nodot (s/fn :all [X Y] poly-map-nodot :- [Y] [f :- (s/=> Y X) xs :- [X]] (map f xs)) - :poly-map-dot (s/fn :all [X Y :.. Z] poly-map-dot :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss))}))) + :poly-map-dot (s/fn :all [X Y :.. Z] poly-map-dot :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & yss :- [Y] :.. Y] (apply map f xs yss)) + :poly-map-dot-arities (s/fn :all [X Y Z S :.. T] + poly-map-dot-arities :- [T] + ([f :- (s/=> T X) xs :- [X]] (map f xs)) + ([f :- (s/=> T X Y) xs :- [X] ys :- [Y]] (map f xs ys)) + ([f :- (s/=> T X Y Z) xs :- [X] ys :- [Y] zs :- [Z]] (map f xs ys zs)) + ([f :- (s/=> T X Y Z S :.. S) xs :- [X] ys :- [Y] zs :- [Z] & ss :- [S] :.. S] (apply map f xs ys zs ss)))}))) (deftest poly-letfn-semantics-test (poly-semantics-test-suite @@ -1721,5 +1775,12 @@ poly-first) :poly-map-nodot (s/letfn [(:all [X Y] poly-map-nodot :- [Y] [f :- (s/=> Y X) xs :- [X]] (map f xs))] poly-map-nodot) - :poly-map-dot (s/letfn [(:all [X Y :.. Z] poly-map-dot :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & xss :- [Y] :.. Y] (apply map f xs xss))] - poly-map-dot)})) + :poly-map-dot (s/letfn [(:all [X Y :.. Z] poly-map-dot :- [Z] [f :- (s/=> Z X Y :.. Y) xs :- [X] & yss :- [Y] :.. Y] (apply map f xs yss))] + poly-map-dot) + :poly-map-dot-arities (s/letfn [(:all [X Y Z S :.. T] + poly-map-dot-arities :- [T] + ([f :- (s/=> T X) xs :- [X]] (map f xs)) + ([f :- (s/=> T X Y) xs :- [X] ys :- [Y]] (map f xs ys)) + ([f :- (s/=> T X Y Z) xs :- [X] ys :- [Y] zs :- [Z]] (map f xs ys zs)) + ([f :- (s/=> T X Y Z S :.. S) xs :- [X] ys :- [Y] zs :- [Z] & ss :- [S] :.. S] (apply map f xs ys zs ss)))] + poly-map-dot-arities)})) From 129a67644524ab5c92b115227d2017ba4d49ecc8 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 11:47:08 -0400 Subject: [PATCH 09/12] ws --- test/cljc/schema/core_test.cljc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/cljc/schema/core_test.cljc b/test/cljc/schema/core_test.cljc index 118411ad..fe0ad0da 100644 --- a/test/cljc/schema/core_test.cljc +++ b/test/cljc/schema/core_test.cljc @@ -1634,8 +1634,7 @@ (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] 2)) (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] [2] 3)) (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] [2] [3] 4)) - (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] [2] [3] [4] 5)) -) + (s/with-fn-validation (invalid-call! poly-map-dot-arities + [1] [2] [3] [4] 5))) (s/defn :all [x] args-shadow-schema-variables :- x From 49c244d7994806a1c041a5ea553c16cc1a8fb9db Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 11:48:29 -0400 Subject: [PATCH 10/12] fix test suite --- test/cljc/schema/core_test.cljc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cljc/schema/core_test.cljc b/test/cljc/schema/core_test.cljc index fe0ad0da..9caf8ee6 100644 --- a/test/cljc/schema/core_test.cljc +++ b/test/cljc/schema/core_test.cljc @@ -1610,7 +1610,8 @@ poly-identity poly-first poly-map-nodot - poly-map-dot]} :- PolySemanticsTestSuite] + poly-map-dot + poly-map-dot-arities]} :- PolySemanticsTestSuite] (is (= 1 (s/with-fn-validation (args-shadow-schema-variables 1)))) (is (= 1 (s/with-fn-validation (poly-identity 1)))) (is (= :a (s/with-fn-validation (poly-identity :a)))) From 8830e657cd57956cd7928a2388a80bc824265605 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 11:52:51 -0400 Subject: [PATCH 11/12] doc --- src/clj/schema/macros.clj | 2 +- src/cljc/schema/core.cljc | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/clj/schema/macros.clj b/src/clj/schema/macros.clj index 1b5770f0..5f339b4e 100644 --- a/src/clj/schema/macros.clj +++ b/src/clj/schema/macros.clj @@ -399,7 +399,7 @@ :arglists (map :arglist processed-arities) :raw-arglists (map :raw-arglist processed-arities) :schema-form (if poly-binder - ;; can't reuse output-schema-sym or schema-bindings as type variables are instantiated via poly-binder-outer-bindings + ;; can't reuse output-schema-sym or schema-bindings since its type variables are instantiated via poly-binder-outer-bindings `(schema.core/all ~poly-binder ~(if (= 1 (count processed-arities)) `(schema.core/->FnSchema ~output-schema ~[(-> schema-bindings first second)]) diff --git a/src/cljc/schema/core.cljc b/src/cljc/schema/core.cljc index 2cb0fff3..c60e5c0d 100644 --- a/src/cljc/schema/core.cljc +++ b/src/cljc/schema/core.cljc @@ -1178,7 +1178,8 @@ each of which is a vector of argument schemas, ending with an optional '& more-schema' specification where more-schema must be a sequence schema. - Dotted schemas may be used as rest schemas, and will be immediately expanded. + Dotted schemas are allowed as the final arguments, and will be expanded into either fixed + or rest arguments upon evaluation. For example, if `Y :..` is in scope then (=> Z X [Y] :.. Y) represents the following functions: (=> Z X) From 71e3c3bc7b22153ac8547216eb8ff272e880b4aa Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Sat, 13 Aug 2022 12:16:04 -0400 Subject: [PATCH 12/12] fix kw --- src/cljc/schema/core.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cljc/schema/core.cljc b/src/cljc/schema/core.cljc index c60e5c0d..13d36e6c 100644 --- a/src/cljc/schema/core.cljc +++ b/src/cljc/schema/core.cljc @@ -1507,7 +1507,7 @@ {:keys [outer-bindings schema-form fn-body arglists raw-arglists]} (macros/process-fn- &env name more-defn-args)] `(let ~outer-bindings (let [ret# (clojure.core/defn ~(with-meta name {}) - ~(assoc (apply dissoc standard-meta ::macros/binder (when (macros/primitive-sym? tag) [:tag])) + ~(assoc (apply dissoc standard-meta ::macros/poly-binder (when (macros/primitive-sym? tag) [:tag])) :doc (str (str "Inputs: " (if (= 1 (count raw-arglists)) (first raw-arglists)