This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2019-02-11
Channels
- # adventofcode (8)
- # announcements (1)
- # arachne (23)
- # beginners (146)
- # boot (4)
- # calva (2)
- # cider (48)
- # cljs-dev (17)
- # clojure (214)
- # clojure-austin (2)
- # clojure-berlin (1)
- # clojure-europe (9)
- # clojure-italy (5)
- # clojure-nl (2)
- # clojure-sanfrancisco (2)
- # clojure-spec (124)
- # clojure-uk (67)
- # clojured (3)
- # clojurescript (95)
- # community-development (7)
- # cursive (68)
- # data-science (1)
- # datomic (80)
- # emacs (19)
- # figwheel (3)
- # figwheel-main (5)
- # fulcro (61)
- # javascript (2)
- # kaocha (1)
- # off-topic (25)
- # pathom (21)
- # pedestal (1)
- # perun (4)
- # reitit (11)
- # ring-swagger (2)
- # shadow-cljs (55)
- # spacemacs (4)
- # sql (8)
- # test-check (16)
- # tools-deps (2)
- # vim (13)
- # yada (4)
how might i go about creating a generator for BigDecimal values? is this a job for fmap?
(defn bigdec? [n] (instance? BigDecimal n))
(s/def :bank/balance bigdec?)
(s/gen :bank/balance)
ExceptionInfo Unable to construct gen at: [] for: :bank/balance clojure.core/ex-info (core.clj:4739)
;; Clojure 1.10.0
(s/exercise decimal?)
=>
([0.5M 0.5M]
[1.0M 1.0M]
[2.0M 2.0M]
...
(defn decimal?
"Returns true if n is a BigDecimal"
{:added "1.0"
:static true}
[n] (instance? BigDecimal n))
maybe stupid question, but whatâs the benefit of s/defop
over a macro?
https://github.com/borkdude/speculative/issues/124#issuecomment-462352972
the benefit is that it forms back to the op
I didn't check the source but reading the comments about it on the dev notes that what it felt like
that is, you have added a new symbol to the spec op language
and yes, itâs a parameterized fixed spec op
not a combinator of arbitrary other spec ops
itâs not intended to cover every possible case, just address one common need
as in the example at https://github.com/clojure/spec-alpha2/wiki/Differences-from-spec.alpha, you have created something with your own semantics
user=> (s/form ::zip)
(user/bounded-string 5 9)
so you retain your semantics
ah. I was just trying this out. I imagined you would get a specific error message for bounded-string:
user=> (s/explain (bounded-string 0 2) "foooo")
"foooo" - failed: (<= 0 (count %) 2)
youâre getting the more specific error messages from the definition of bounded-string
many of the spec ops have internal constraints that will generate explain problems
so this is the same as other spec ops
like regexes check whether the input is nil? or sequential?
keys checks for map?
it may be that some further adjustments should be made in the explain generator (just deferring to the definition spec right now)
I didnât highlight it, but inst-in
, int-in
, and double-in
are all derived specs and I reimplemented all of those in terms of defop
they are effectively all parameterized compound specs
This gives more or less the same result when it comes to error messages:
user=> (defmacro bounded-string2 [min max] `(s/and string? #(<= ~min (count %) ~max)))
#'user/bounded-string2
user=> (s/explain (bounded-string2 0 2) "foooo")
"foooo" - failed: (<= 0 (count %) 2)
but s/form
will give the expanded spec form, so thatâs indeed differentthe other big thing is that with defop, we actually def a macro
well I guess youâll get the same effect if youâre using defmacro here
(vs just implementing the lower-level create-spec
)
@mpenet
> itâs basically a parameterized spec, not 2 different specs
Not sure what you mean by this. If you call (bounded-string 1 10)
and (bounded-string 1 20)
you will get two different spec objects
yeah but the form share quite a bit, I guess the facts it's 2 distinct spec objects is an impl detail
as I said I don't know more than what I read on the blog post + intuition of what's the final intent
for lib authors it's nicer to read (s/sorted-coll-of x comparator)
than (s/and (s/coll-of x) #(..))
when would a lib author read this? not trying to argue for the sake of arguing, just want to get it clear for myself đ
I havenât used s/form
and friends myself yet, so I havenât needed this feature much (probably out of ignorance)
here we have specs that are used to validate json params also, one of which is some kind of derivative of coll-of, it makes dealing with it much nicer too
but compared to a macro you donât see a benefit there yet? I mean when reading and writing the literal code?
more readable s/form is a plus for me, ability to build your own s/coll-of like ops is good too
I mean, when you define a macro you get to write same code:
user=> (defmacro bounded-string2 [min max] `(s/and string? #(<= ~min (count %) ~max)))
#'user/bounded-string2
user=> (bounded-string2 1 100)
#object[clojure.spec_alpha2.impl$and_spec_impl$reify__1407 0x64b3b1ce "clojure.spec_alpha2.impl$and_spec_impl$reify__1407@64b3b1ce"]
user=> (bounded-string 1 100)
#object[clojure.spec_alpha2$op_spec$reify__1022 0x1a0b5323 "clojure.spec_alpha2$op_spec$reify__1022@1a0b5323"]
> ability to build your own s/coll-of like ops is good too
this is limited with s/defop
since you cannot pass in any spec argument. e.g. (my-coll-of (s/nilable ::my-spec))
wonât work?
I was referring to this link that I posted earlier: https://github.com/borkdude/speculative/issues/124#issuecomment-462261842
int-in
, inst-in
, double-in
are all good examples where this is useful too
they are all compound parameterized specs
I just mean general cases where s/defop is better
symbolic specs form a language, defined by the ops
using a macro that expands to a compound spec is fine - youâre using an initial step to produce something in the language
using defop lets you create new ops in the language for the special case where the op can be defined in terms of other spec ops (and is not parameterized by another spec)
and if you need that, you can drop down another level and do the same thing the provided ops are doing - implement the create-spec multimethod to return a Spec protocol instance
(but a lot more code is required)
is nilable
an op? then why can (s/nilable ::foo)
not be passed as an argument to defop, but ::foo
can?
sorry, bit confused about âsymbolic specsâ
nilable is an op
as I said above, defop is not designed to be parameterized by other symbolic specs
whatâs the example?
it really goes to the evaluation model. spec names (fq keywords) are a little special in that they eval to themselves and also they are the only thing other than a spec object explicitly handled in the spec api functions.
in this case, ::foo evaluates to a valid symbolic spec (where another spec form evaluates to a spec object, which is not a symbolic spec)
so it will work, but youâve created a narrow constraint on how it can be used
right, so itâs something that happens to work, but not really the common use case for defop
yeah, I hadnât really thought about that
youâre also not going to get proper explain path-ing with it as a spec created by defop is considered to be a single op
so if the sub-spec fails, you wonât get the parent spec in the path
ok. to conclude: defop is not designed to support spec arguments. If you want that, either write a macro and accept less helpful error messages and s/form output, or âdrop down another level and do the same thing the provided ops are doing - implement the create-spec multimethod to return a Spec protocol instanceâ which requires more code
although I think in the macro case, the errors are almost exactly the same
so I would maybe quibble with that part
I notice that regular pre-defined predicates are also not supported in defop
:
user=> (s/defop first-pred [pred] (s/and (pred (first %))))
#'user/first-pred
user=> (s/valid? (first-pred number?) [1 "f"])
Maybe a bad exampleagain, evaluation
the definition in defop is not going to evaluated - it should be a valid symbolic spec but where the parameterized values are substituted (defop is literally going through and replacing the args with their values)
Made a typo. This works:
user=> (s/defop first-pred [pred] (s/and #(pred (first %))))
user=> (s/valid? (first-pred number?) [1 "f"])
true
For specs parameterized by other specs, you can do something like
(defn domain-keywords
"Given a spec, return a new spec that can conform a keyword or string to
a keyword that is constrained by the given spec."
[spec]
(s/spec* `(s/with-gen (s/and ::keyword ~spec)
(fn [] (g/fmap name (s/gen ~spec))))))
(that and bounded-string
above come from the World Singles Networks' codebase)@seancorfield what benefit does that have over writing domain-keywords
as a macro?
well, itâs a function so you can apply it, so usual benefits of function over macro
(with the caveat that the spec arg needs to be a form, not an object, here)
It was a function in the spec1 world (without (s/spec* ..)
and the quote/unquote, so we made it a function in the spec2 world. Minimal change (and it still composes and applies etc).
@seancorfield I had a similar thing with seqable-of
. Function in spec1, but when I turned it into a function in spec2 using s/spec*
, I could not provide specs like (s/nilable ::foo)
because I got an error, so then I made it a macro.
I plan on writing up a (probably long) blog post on our conversion from spec1 to spec2 when Alex tells me spec2 is stable enough for that to be widely valuable đ
you canât use this with in s/def though (like (s/def ::x (domain-keywords ...))
)
Right. And we use s/defop
for those sorts of things.
but you could with the functional entry point s/register
which takes an object (which is what domain-keywords
returns)
@seancorfield btw, I spent some time working on the regex thing and I need to undo the changes I made to support forward references in regexes
which will fix the nesting issue, but re-break forward references
@borkdude Mostly, we've found changing our defn
spec builders over to s/defop
has been all we've needed in the most common cases. A few have needed s/spec*
instead.
@alexmiller That's fine -- the forward reference issue only affected one spec in our entire code base so I can just move it below the sub-specs đ
Forward refs in regex is solvable via different means but I need to talk to Rich before I commit to a path on that and heâs out today
@borkdude "those sorts of things" = "use this with in s/def"
I canât write a very long blogpost about transitioning to spec2. All I had to do is report bugs, which were all fixed, wrap a bunch of predicates in s/spec
, refactor one predicate to #(not (sequential %))
instead of (complement sequential?)
and turn a private function into a macro.
@seancorfield reverted the fwd reference fix, which should fix the nesting issue (but break that fwd reference)
yes, in many cases
in general, specs delay lookup of named specs until use
I fixed several places where that wasnât being done
but changes in how regexes are implemented mean that we effectively broke it for them
regex impls used to not be specs but would get spec-ized when needed. in spec 2, regexes actually are spec instances (thanks to metadata protocol implementation!) which simplifies the code in many places, but removed the point where this delay naturally happened before
fixing it is ⌠tedious
yeah, no thanks :)
@alexmiller Good to know. I'll run a full test suite with the latest spec2 shortly.
@alexmiller Confirming that our full test suite runs on the latest spec2, with that one forward reference regex spec moved after the specs that it refers to.
:thumbsup: