I think I may have a problem with the correct order of nesting Specs within a function - specifically s/with-gen and s/or...
I have this function and Spec:
(defn set-gift-pair-in-gift-history [g-hist g-year g-pair]
(if (nil? g-hist)
[{:giver :none, :givee :none}]
(assoc g-hist g-year g-pair)))
(s/fdef set-gift-pair-in-gift-history
:args (s/with-gen
(s/or :input-hist (s/and
(s/cat :g-hist :unq/gift-history
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %))))
:input-nil (s/and
(s/cat :g-hist nil?
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %)))))
#(gen/let [hist (s/gen :unq/gift-history)
year (gen/large-integer* {:min 0 :max (max 0 (dec (count hist)))})
pair (s/gen :unq/gift-pair)]
[hist year pair]))
:ret :unq/gift-history)
Which tests correctly:
(stest/check `set-gift-pair-in-gift-history)
=>
({:spec #object[clojure.spec.alpha$fspec_impl$reify__2451
0x729d93b6
"clojure.spec.alpha$fspec_impl$reify__2451#729d93b6"],
:clojure.spec.test.check/ret {:result true,
:num-tests 1000,
:seed 1531413555637},
:sym clojure-redpoint.roster/set-gift-pair-in-gift-history})
And its parameters conform correctly:
(s/conform (s/or :input-hist (s/and
(s/cat :g-hist :unq/gift-history
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %))))
:input-nil (s/and
(s/cat :g-hist nil?
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %)))))
[[{:giver :GeoHar, :givee :JohLen}] 0 {:giver :RinStaXX, :givee :PauMccXX}])
=>
[:input-hist
{:g-hist [{:giver :GeoHar, :givee :JohLen}],
:g-year 0,
:g-pair {:giver :RinStaXX, :givee :PauMccXX}}]
(s/conform (s/or :input-hist (s/and
(s/cat :g-hist :unq/gift-history
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %))))
:input-nil (s/and
(s/cat :g-hist nil?
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %)))))
[nil 0 {:giver :RinStaXX, :givee :PauMccXX}])
=>
[:input-nil
{:g-hist nil, :g-year 0, :g-pair {:giver :RinStaXX, :givee :PauMccXX}}]
But when this "correct" function is consumed by a second function:
(defn set-gift-pair-in-roster [plrs-map plr-sym g-year g-pair]
(let [plr (get-player-in-roster plrs-map plr-sym)
gh (get-gift-history-in-player plr)
ngh (set-gift-pair-in-gift-history gh g-year g-pair)
nplr (set-gift-history-in-player ngh plr)]
(assoc plrs-map plr-sym nplr)))
(s/fdef set-gift-pair-in-roster
:args (s/cat :plrs-map ::plr-map
:plr-sym keyword?
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
:ret ::plr-map)
The consumed function becomes a source of error for the consuming function (the nil case - which I thought was handled):
(stest/check `set-gift-pair-in-roster)
=>
({:spec #object[clojure.spec.alpha$fspec_impl$reify__2451
0x3bbc704a
"clojure.spec.alpha$fspec_impl$reify__2451#3bbc704a"],
:clojure.spec.test.check/ret {:result #error{:cause "Call to #'clojure-redpoint.roster/set-gift-pair-in-gift-history did not conform to spec:
In: [0] val: nil fails spec: :unq/gift-history at: [:args :input-hist :g-hist] predicate: vector?
val: {:g-hist nil, :g-year 1, :g-pair {:givee :_+, :giver :RJK/Y24}} fails at: [:args :input-nil] predicate: (<= (:g-year %) (count (:g-hist %)))
I have tried changing the order and grouping (nesting) of the specs in the consumed function - but then it fails the tests that it used to pass, before even getting to testing the consuming function.
Any thoughts on what is going wrong here?
Thank you!
EDIT:
As suggested, here is the full code for a better understanding:
;Here is an example of The Beatles keeping track of the Xmas gifts
;they give to each other (:giver and :givee) each year over time:
(ns clojure-redpoint.roster2
(:require [clojure.spec.alpha :as s]
[orchestra.spec.test :as st]
[clojure.test.check.generators :as gen]
[clojure.spec.test.alpha :as stest]))
(s/def ::givee keyword?)
(s/def ::giver keyword?)
(s/def :unq/gift-pair (s/keys :req-un [::givee ::giver]))
(s/def ::name string?)
(s/def :unq/gift-history (s/coll-of :unq/gift-pair :kind vector?))
(s/def :unq/player (s/keys :req-un [::name :unq/gift-history]))
(s/def ::plr-map (s/map-of keyword? :unq/player))
(defn- get-player-in-roster [plrs-map plr-sym]
(get plrs-map plr-sym))
(s/fdef get-player-in-roster
:args (s/cat :plrs-map ::plr-map :plr-sym keyword?)
:ret (s/or :found :unq/player
:not-found nil?))
(defn- get-gift-history-in-player [plr]
(get plr :gift-history))
(s/fdef get-gift-history-in-player
:args (s/or :input-plr (s/cat :plr :unq/player)
:input-nil (s/cat :plr nil?))
:ret (s/or :found :unq/gift-history
:not-found nil?))
(defn set-gift-pair-in-gift-history [g-hist g-year g-pair]
(if (nil? g-hist)
[{:giver :none, :givee :none}]
(assoc g-hist g-year g-pair)))
(s/fdef set-gift-pair-in-gift-history
:args (s/with-gen
(s/or :input-hist (s/and
(s/cat :g-hist :unq/gift-history
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %))))
:input-nil (s/and
(s/cat :g-hist nil?
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %)))))
#(gen/let [hist (s/gen :unq/gift-history)
year (gen/large-integer* {:min 0 :max (max 0 (dec (count hist)))})
pair (s/gen :unq/gift-pair)]
[hist year pair]))
:ret :unq/gift-history)
(defn set-gift-history-in-player [g-hist plr]
(if (or (nil? g-hist) (nil? plr))
{:name "none", :gift-history [{:giver :none, :givee :none}]}
(assoc plr :gift-history g-hist)))
(s/fdef set-gift-history-in-player
:args (s/or :input-good (s/cat :g-hist :unq/gift-history
:plr :unq/player)
:input-hist-nil (s/cat :g-hist nil?
:plr :unq/player)
:input-plr-nil (s/cat :g-hist :unq/gift-history
:plr nil?)
:input-both-nil (s/cat :g-hist nil?
:plr nil?))
:ret :unq/player)
(defn set-gift-pair-in-roster [plrs-map plr-sym g-year g-pair]
(let [plr (get-player-in-roster plrs-map plr-sym)
gh (get-gift-history-in-player plr)
ngh (set-gift-pair-in-gift-history gh g-year g-pair)
nplr (set-gift-history-in-player ngh plr)]
(assoc plrs-map plr-sym nplr)))
(s/fdef set-gift-pair-in-roster
:args (s/cat :plrs-map ::plr-map
:plr-sym keyword?
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
:ret ::plr-map)
(st/instrument)
(def roster-map
{:RinSta {:name "Ringo Starr", :gift-history [{:giver :RinSta, :givee :PauMcc}]},
:JohLen {:name "John Lennon", :gift-history [{:giver :JohLen, :givee :GeoHar}]},
:GeoHar {:name "George Harrison", :gift-history [{:giver :GeoHar, :givee :JohLen}]},
:PauMcc {:name "Paul McCartney", :gift-history [{:giver :PauMcc, :givee :RinSta}]}})
(s/conform ::plr-map
(set-gift-pair-in-roster roster-map :PauMcc 0 {:giver :JohLenXXX, :givee :GeoHarXXX}))
;=>
;{:RinSta {:name "Ringo Starr", :gift-history [{:giver :GeoHar, :givee :JohLen}]},
; :JohLen {:name "John Lennon", :gift-history [{:giver :RinSta, :givee :PauMcc}]},
; :GeoHar {:name "George Harrison",
; :gift-history [{:giver :PauMcc, :givee :RinSta}]},
; :PauMcc {:name "Paul McCartney",
; :gift-history [{:giver :JohLenXXX, :givee :GeoHarXXX}]}}
;(stest/check `set-gift-pair-in-roster)
Unfortunately, it did not help me find my error...
The problem is that one of your instrumented functions set-gift-pair-in-gift-history is being called with invalid arguments when you (stest/check `set-gift-pair-in-roster):
CompilerException clojure.lang.ExceptionInfo: Call to #'playground.so/set-gift-pair-in-gift-history did not conform to spec:
In: [0] val: nil fails spec: :unq/gift-history at: [:args :input-hist :g-hist] predicate: vector?
val: {:g-hist nil, :g-year 1, :g-pair {:givee :A, :giver :A}} fails at: [:args :input-nil] predicate: (<= (:g-year %) (count (:g-hist %)))
The check output gives us a minimal input to reproduce the error:
(set-gift-pair-in-roster {} :A 1 {:givee :A, :giver :A})
We can see the first argument to the failing function is nil. Looking at set-gift-pair-in-gift-history's function spec, there's a suspect spec that covers that case:
:input-nil (s/and
(s/cat :g-hist nil?
:g-year (s/and int? #(> % -1))
:g-pair :unq/gift-pair)
#(<= (:g-year %) (count (:g-hist %)))))
This will only conform when g-hist is nil and g-year is 0, but the generator for :g-year is going to generate many numbers besides 0. That's why the calls to it fail when it's instrumented.
instrument and check have shown a discrepancy between how the program is specified to behave and how it actually behaves. I'd start by thinking about how set-gift-pair-in-gift-history should be spec'd when its first argument is nil. The implementation doesn't care about the other arguments when the first arg is nil, so you could adjust the function spec to reflect that:
:input-nil (s/cat :g-hist nil? :g-year any? :g-pair any?)
With that change, your top-level function should check successfully.
Related
I need to match two kinds of tuples and produce maps from them.
Both have a keyword and a string. One can have a third item (a language code).
[<key> <value>] ~> {:type <key> :value <value>}
[<key> <value> <lang>] ~> {:type <key> :value <value> :lang <lang>}
I only need to match those which keyword is either :foo or :bar and decided that I would use clojure.core.match:
(ns so.example
(:require
[clojure.core.match :refer [match]]))
(defn example-1 [ast]
(let [l10n-key #{:foo :bar}]
(match ast
[(k :guard l10n-key) v lang] {:type k :value v :lang lang}
[(k :guard l10n-key) v] {:type k :value v})))
(example-1 [:foo 10])
;=> {:type :foo, :value 10}
(example-1 [:bar 20 "en"])
;=> {:type :bar, :value 20, :lang "en"}
That works but I wanted to reuse the matching pattern :guard l10n-key in different clauses. So I thought I could use some syntax quoting and unquote splicing:
(defn example-2 [ast]
(let [l10n-key-match [:guard #{:foo :bar}]]
(match ast
[`(k ~#l10n-key-match) v lang] {:type k :value v :lang lang}
[`(k ~#l10n-key-match) v] {:type k :value v})))
However the defn expression crashes with:
Unexpected error (AssertionError) macroexpanding match at (form-init11111096422056977084.clj:3:5).
Invalid list syntax (clojure.core/concat (clojure.core/list (quote so.example/k)) l10n-key-match) in (clojure.core/seq (clojure.core/concat (clojure.core/list (quote so.example/k)) l10n-key-match)). Valid syntax: [[:default :guard] [:or :default] [:default :only] [:default :seq] [:default :when] [:default :as] [:default :<<] [:default :clojure.core.match/vector]]
What am I doing wrong?
Isn't this what spec, that already ships with Clojure, does? You would define your pattern like
(ns playground.catspec
(:require [clojure.spec.alpha :as spec]))
(spec/def ::type #{:foo :bar})
(spec/def ::value number?)
(spec/def ::lang #{"en" "sv" "fr"})
(spec/def ::key-value-lang (spec/cat :type ::type
:value ::value
:lang (spec/? ::lang)))
We use spec/def to define a spec, spec/cat to concatenate specs and spec/? for a spec that is optional.
Then we use conform to parse the tuple:
(spec/conform ::key-value-lang [:foo 10])
;; => {:type :foo, :value 10}
(spec/conform ::key-value-lang [:bar 20 "en"])
;; => {:type :bar, :value 20, :lang "en"}
(spec/conform ::key-value-lang [:bar 119 "fr"])
;; => {:type :bar, :value 119, :lang "fr"}
(spec/conform ::key-value-lang [119 :foo])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang [:bar 119 "uj"])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang [:bar])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang [:bar 119 "fr" :asdfasdf])
;; => :clojure.spec.alpha/invalid
(spec/conform ::key-value-lang {:a 1 :b 4})
;; => :clojure.spec.alpha/invalid
Doesn't solve the problem with clojure.core.match, but you don't really need it for something this simple:
(ns tst.demo.core
(:use tupelo.core tupelo.test))
(dotest
(let [data [[:foo 10]
[:bar 20 "en"]
[:fizz 10]
[:buzz 20 "en"]]
keep-tags #{:foo :bar}
data-keep (filterv #(contains? keep-tags (first %)) data)
result (forv [tuple data-keep]
(zipmap [:type :value :lang] tuple))]
(is= result [{:type :foo, :value 10}
{:type :bar, :value 20, :lang "en"}])))
You may also be interested in these helper functions:
tupelo.core/matches? A nicer interface to clojure.core.match
tupelo.core/submatch?
tupelo.core/wild-match?
tupelo.core/wild-submatch?
(s/def ::users (s/coll-of ::user :distinct true))
The spec above requires each user map to be distinct but How can I specify it to check for distinct :user/ids only
The collection bellow shouldn't be allowed:
[{:id 10 :name "Jessica"} {:id 10 :name "Erica"}]
(s/def ::id (s/int-in 0 40)) ; just for testing purposes
(s/def ::name string?)
(s/def ::user (s/and (s/keys :req-un [::id ::name])))
(s/def ::user-list (s/and
(s/coll-of ::user :distinct true :into [])
#(if (empty? %) true (apply distinct? (mapv :id %)))))
(deftest so-test
(let [users [{:id 11 :name "Jessica"} {:id 11 :name "Erica"}]]
(prn (g/generate (s/gen ::user-list)))
(s/assert ::user-list users)))
I'm parsing some Hiccup in CLJS, with the goal of taking :h2 and :h3 elements and converting them to a tree of nested :ul and :li.
My starting point is a flat vector like:
[[:h2 {} "Foo"] [:h2 {} "Bar"] [:h3 {} "Child1"] [:h2 {} "Baz"]]
If I just map over these and replace (first el) with [:li], I have a flat list. But I'd like to get something like:
[[:li "Foo"] [:li "Bar"] [:ul [:li "Child1"]] [:li "Baz"]]
If I call (partition-by #(= :h2 (first %)) my-vec), I get something almost useful:
(([:h2 {} "Foo"] [:h2 {} "Bar"]) ([:h3 {} "Child1"]) ([:h2 {} "Baz"]))
The partition happens when the predicate #(= :h2 (first %)) changes, (which is what the documentation says it does).
How can I get the behavior I'm looking for?
Here is one way to do it:
(def data [
[:h2 {} "Foo"]
[:h2 {} "Bar"]
[:h3 {} "Child1"]
[:h2 {} "Baz"] ] )
(defn formatter [elem]
(condp = (first elem)
:h2 [:li (last elem)]
:h3 [:ul [:li (last elem)]]
))
(newline) (println :data data)
(newline) (println :result (mapv formatter data))
with result
:data [[:h2 {} Foo] [:h2 {} Bar] [:h3 {} Child1] [:h2 {} Baz]]
:result [[:li Foo] [:li Bar] [:ul [:li Child1]] [:li Baz]]
Update:
Rewrite like so to get all the :h3 items in one :ul
(def data [
[:h2 {} "Foo"]
[:h3 {} "Child1"]
[:h2 {} "Bar"]
[:h3 {} "Child2"]
[:h3 {} "Child3"]
[:h2 {} "Baz"] ] )
(defn h2? [elem]
(= :h2 (first elem)))
(defn ->li [elem]
[:li (last elem)])
(defn fmt [data]
(let [h2 (filter h2? data)
h3 (filter #(not (h2? %)) data)
result (conj (mapv ->li h2)
(apply vector :ul (mapv ->li h3))) ]
result ))
(newline) (println :data data)
(newline) (println :result (fmt data))
with result
:data [[:h2 {} Foo] [:h3 {} Child1] [:h2 {} Bar] [:h3 {} Child2] [:h3 {} Child3] [:h2 {} Baz]]
:result [[:li Foo] [:li Bar] [:li Baz] [:ul [:li Child1] [:li Child2] [:li Child3]]]
Here's an answer that does the job, but is horribly inelegant, since it essentially mutates the last element in the reduce call when necessary:
(defn listify-element [element]
"Replaces element type with :li."
(vec (concat [:li (last element))]))
(defn listify-headings [headings-list]
"Takes subitems (in :h2 :h3) and creates sub :uls out of the :h3 lists."
(vec
(concat
[:ul]
(map-indexed
(fn [ind headings]
(if (= 0 (mod ind 2))
(map listify-element headings)
(vec (concat [:ul] (map listify-element headings)))))
(partition-by #(= :h2 (first %)) headings-list)))))
(defn nest-listified-headings [vector-list]
"Nests sub-:uls inside their preceding :lis."
(vec (concat [:ul]
(reduce
(fn [acc el] (if (= (first el) :ul)
(conj (pop (vec acc)) (conj (last acc) el))
(concat acc el)))
vector-list))))
Produces:
(nest-listified-headings
(listify-headings [[:h2 "Foo"] [:h2 "Bar"] [:h3 "Baz"] [:h3 "Bat"]])
[:ul [:li "Foo"]
[:li "Bar"
[:ul
[:li "Baz"]
[:li "Bat"]]]]
I have:
(def moo (my-func))
which returns:
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}]
How do I now access moo to get the :name where :id=3? Thanks.
I would rather prefer using some (since it is more logical than using filter i guess, because it is designed to find exactly one value):
(def data
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}])
(defn name-by-id [id data]
(some #(when (= (:id %) id) (:name %)) data))
user>
(name-by-id 3 data)
"Greg"
user>
(name-by-id 100 data)
nil
One way would be to use a filter
(def moos
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}])
(defn name-for-id
[id]
(:name (first (filter #(= (:id %) id) moos))))
(name-for-id 3) ; => "Greg"
(def names
[{:id 1 :name "Bob"}
{:id 2 :name "Jane"}
{:id 3 :name "Greg"}])
;;get the :name where :id=3
(defn answer []
(:name (first (filter (fn [e] (= 3 (:id e))) names))))
In the above rather than names you could have moo.
Please look at the following code:
(def data {:color ["R", "B", "G"] :name "Hello" :up "down"})
(defn collapse-vector-kvp [k v]
(map #(hash-map k %) v))
(defn collapse-map [m]
(map #(let
[x %]
(if (vector? (val x))
(collapse-vector-kvp (key x) (val x))
(hash-map (key x) (val x))
)) m))
(collapse-map data)
=> ({:name "Hello"} ({:color "R"} {:color "B"} {:color "G"}) {:up "down"})
What I would like to do is create a single list, rather than have the 'color' entries be in a list within the list. Is this easily achievable?
user=> (def data2 '({:name "Hello"} ({:color "R"} {:color "B"} {:color "G"}) {:up "down"}))
#'user/data2
user=> (flatten data2)
({:name "Hello"} {:color "R"} {:color "B"} {:color "G"} {:up "down"})
Another version of collapse-map:
(defn collapse-map [m]
(let [sep-m (group-by (comp vector? val) m)]
(concat (map (fn [[k v]] {k v})
(sep-m false))
(apply concat (map (fn [[k v]]
(collapse-vector-kvp k v))
(sep-m true))))))
(def test-data {:color ["R" "B" "G"]
:name "Hello"
:k ["v1" "v2" "v3"]
:up "down"})
(collapse-map test-data)
=> ({:name "Hello"}
{:up "down"}
{:color "R"}
{:color "B"}
{:color "G"}
{:k "v1"}
{:k "v2"}
{:k "v3"})