Fork me on GitHub
#clojure-spec
<
2019-09-05
>
pvillegas1211:09:46

I have a spec which reuses another, something like

(s/def ::a
  (s/and #(s/valid? ::sub-a %)
  (s/keys :req [:a/only-attribute]))

pvillegas1211:09:22

However, my problem is that s/explain does not show the ::sub-a spec violations, it just says that ::sub-a is invalid

pvillegas1211:09:38

is there a way for spec to “walk” down ::sub-a in this case?

pvillegas1211:09:06

I could, of course, copy over the (s/keys) in ::sub-a to the spec of ::a but I was looking for a solution that reuses the ::a declaration

Alex Miller (Clojure team)12:09:30

Right now, this is what I would recommend. Much better options coming for this in spec 2

pvillegas1212:09:09

I actually got good error messages by doing

(s/def ::a
  ::sub-a
  (s/keys :req [:a/only-attribute]))

Alex Miller (Clojure team)12:09:54

That looks invalid, what are you seeing?

pvillegas1212:09:19

I’m actually getting the error messages for ::sub-a should that not work?

Leon11:09:36

how can i describe a map where a key could either contain a normal value ::some-object or, in an error-case, an error-value? I don't want to add the error-case to ::some-object as that would greatly decrease the expressiveness and reliability,...

Alex Miller (Clojure team)11:09:09

Spec is about expressing the true range of values. If it can contain special error values, then that is part of that truth

Alex Miller (Clojure team)12:09:21

If that’s weird, you might consider using an alternate attribute for the error or using exceptions

Leon12:09:51

exceptions are an extremely ugly solution (at least for me, coming from ADT Either-type land)... and my data cannot contain errors, but the place they're transmitted can. i guess i'll create a wrapper object that can be either of type success or of type failure, and then contain the values respectively

Alex Miller (Clojure team)12:09:23

Exceptions are the idiomatic solution on the jvm and in Clojure

Leon12:09:18

i guess thats an idiom i dislike ^^ i really hate the complexity that braking out of my control flow adds, especially when otherwise working with pure functions that are easy to reason about ;D and in my case exceptions aren't really a simple option, as im working in clojurescript with re-frame, where im dispatching events and then dispatching other events on success

potetm12:09:06

Problem is: if you go down the path of “exceptions are bad,” you’re fighting not just the clojure ecosystem, but the Java(Script) one. Leveraging host libs is one of the primary reasons clojure(script) exists at all!

potetm12:09:01

Not to say you can’t write clj(s) w/o host libs. Many try. But know that you’re giving up a primary value-add for the lang.

potetm12:09:03

Many have also tried to improve upon exceptions (e.g. slingshot). I’ve found them to be marginal gains at best. Not worth their overhead.

Leon14:09:38

yea i know, i don't have any direct issue with exceptions, i just really hate using them where not necessary. and as i said, in my case, they are pretty much not a solution, as the one observing the result isn't the same thing that calls the thing that could throw. for such cases, where error-data makes more sense than exceptions, some solution would really be great. but with the current a spec only talks about the keys it gets, the type of the values HAVE to be defined as part of that key really makes something like this nearly impossible, and strongly increases the complexity in knowing what shape your data actually has, because i CANNOT actually spec it without introducing additional complexity (and removing a hell of a lot of expressivity) to my domain model

Alex Miller (Clojure team)14:09:31

if your data can contain error values, then spec'ing it means spec'ing those values. spec is just exposing that complexity that you have introduced

Leon14:09:58

my data cannot contain errors. a book doesn't contain any error. a find-book-response can contain that error. the big problem is that spec forces me to define find-book-response specs for everything, and by that introduces an additional layer of complexity where something like generics could really help. the whole notion of needing to have a spec for a key in a map makes many things really complicated for no good reason. say you have a utility function, that takes a (s/keys*) map as arguments (maybe it does some complex math where you want the arguments to be named for readability. now you need to create spec defs for each int? that the function can take, thus adding extreme amounts of complexity where a (s/kv :req {:num1 int?, :num2 float?}) would solve your problem perfectly.

Leon14:09:13

enforcing best practices is a good thing, don't get me wrong. but if these best practices only apply in some areas, but you enforce them in every little situation, that really screws up what could have been an incredibly powerful and nice solution to safety and type-validation in a fully dynamic environment

Alex Miller (Clojure team)14:09:47

I don't understand what's forcing you to define anything

Alex Miller (Clojure team)14:09:12

keys specs are open - you don't have to spec every attribute

Leon14:09:01

say you have some complex, user-input based form, where the data doesn't really mean anything specific from a domain-model point of view. if you want to validate that data in a map context, you'll need to define seperate, single use specs for each entry in that map that you want to validate.

Leon14:09:47

I'd say that "just don't spec things that would take to much effort" isn't a desirable solution.

Leon14:09:42

not only does your safety suffer from that, but it also defeats the purpose of spec in general. because then you could just say "just manually write (if (not (int? my-number)) (throw Exception. "foo")),... thats not the point

Leon14:09:49

the point is to make validations easier and expressive

Alex Miller (Clojure team)14:09:09

well yes, but not for every purpose

Alex Miller (Clojure team)14:09:49

given that these are not for your particular domain, but a truly dynamic problem, you're a bit off the goals of spec

Alex Miller (Clojure team)14:09:18

you certainly could use a pairing of fields and specs to generically validate (but don't validate the aggregate map)

Alex Miller (Clojure team)14:09:37

then no registration is needed

Leon14:09:09

still, what is the actual reason there is no spec-function that takes keys ( be they namespace qualified or not) and their types (predicates, etc) and validates a map against them? is it just to encourage good practises?

Alex Miller (Clojure team)14:09:31

I don't know that I explain it better than Rich, who has written/talked about this extensively

Leon14:09:50

could you give me a summary?

Alex Miller (Clojure team)14:09:03

the whole idea is to imbue semantics to attributes, and register those for use globally

Alex Miller (Clojure team)14:09:20

and to de-emphasize the role of the aggregate

Alex Miller (Clojure team)14:09:31

that is, the attributes are the driver, not the aggregation (the map)

Alex Miller (Clojure team)14:09:51

this is taken much further in the schema/select work in spec 2 (and s/keys is going to go away)

Alex Miller (Clojure team)14:09:11

spec 2 schemas do have inline support for ad hoc un-namespaced key specs

👏 4
Alex Miller (Clojure team)14:09:34

the ability to programatically create and use specs is greatly enhanced in spec 2 as well

Leon14:09:15

so spec 2 will support that kind of thing i'm requesting? as i said, i fully understand the "attributes are the driver, not the aggregation" argument, and i think as a base philosophy it's actually rather genius. but there still are a lot of cases where these attribute-semantics are single-use function arguments. and in these cases, having to give names to things that only make sense in a very specific context is increasing complexity and hurting readability. the "attributes are the driver" argument stops working as soon as you leave your direct domain-model and go into functions that achieve very specific purposes with specific parts of that data. having to describe the shape of every little thing there really makes this a lot more complicated. especially in my current project, a clojurescript re-frame based frontend application, i really want to spec out every view function. not strictly for validation or domain-expressivity, but actually primarily just to get any kind of good errors. knowing that a view function got its arguments in the wrong shape/type is a lot more helpful than knowing that some reagent-internal had problems.... for these purposes spec would nearly be perfect. dynamic validation, that is actually close to dependent type-systems, would be the perfect solution to greatly specify what each function expects as arguments, no matter how detached that function is from the domain model. but the way that spec currently prohibits the generation of map-specs inline makes a lot of things hugely more complex. having a view function that takes 4 arguments look like this:

(s/fdef foo
  :args (s/cat :title string?, :subtitle string?, :rating ::domain/rating, :content string?))

(defn foo [title subtitle rating content]
  ...)
is great for the spec. but calling the function gets anoyying and unreadable, so you want to add keyworded args:
(s/def ::title string?)
(s/def ::subtitle string?)
(s/def ::content string?)

(s/fdef foo
  :args (s/cat :props (s/keys* :req-un [::title ::subtitle ::domain/rating ::content])))

(defn foo [& {:keys [title subtitle rating content]}]
  ...)
this is easier to call and read from caller side.... but extremely unnecessarily complex from implementation side. (yes, title subtitle and content could be domain specific stuff, but maybe im writing a component library that doesn't HAVE any domain specific information.) do you see where i come from?

Alex Miller (Clojure team)14:09:27

those seem almost the same. so not sure I'm getting "extremely unnecessarily complex"

Alex Miller (Clojure team)14:09:43

I don't think spec 2, at this exact moment, helps in this particular situation. however, it's not really easy to judge as we are currently completely reworking how function specs are defined and used and it's probably not going to bear much similarity to spec 1

Alex Miller (Clojure team)14:09:46

something like (s/schema {:title string? :subtitle string? :rating int? :content string?}) is sufficient at current moment to spec a map with those (unqualified) keys. can't currently apply that ala s/keys* - that's still a tbd right now (I don't think it's a big deal to address, just not current focus)

Leon17:09:29

In this case it's not that Bad, but in more complex cases, and especially all over the Code, it gets very anoyying to type and Deal with. It would be great if that could be Adressed in spec2, as it then would be useful in pretty much any Situation without being overly verbose

andy.fingerhut17:09:15

Sorry, I do not have much constructive to say, but I do recall an explicit statement made by one of the Clojure core team (probably Stu Halloway) that something can be simple (i.e. not complex) but still be verbose.

Leon19:09:41

yes, thats the java philosophy. "make simple things seem 5 times as complex by introducing unnecessary abstractions, syntactical overhead and overengineered design patterns". that its POSSIBLE to make something simple overly verbose isn't a good thing, and absolutely nothing you should WANT