Fork me on GitHub
#clojure-spec
<
2021-04-14
>
vlaaad13:04:04

Is there a way to make fn spec that allows fn to throw exceptions? I want to express a contract "this function returns any value and can even throw"

avi13:04:43

IIRC, no.

avi13:04:43

This has encouraged me to spec out some of my functions to return values that indicate success or failure. Often they’re :cognitect.anomalies/anomaly values. I’m pretty happy with this approach. It makes the functions more testable, I think. And by speccing out the error values you get the property testing to extend to error cases as well.

vlaaad13:04:09

Yeah, but I want to document that I allow user-provided callback to throw an exception. It’s like a promise “it’s okay to fail”…

avi13:04:40

Yeah, makes sense, reasonable. I just think that’s out of scope for spec.

avi13:04:53

If I were to speculate the thinking behind that call, I’d guess that it’s an example of the position that exceptions should be used in truly exceptional cases — cases that are hard to predict and handle — and can thus be thrown anywhere, at any time. (That’s a reductive summary missing much nuance but I hope potentially slightly useful.) Just speculation though, I could easily be mistaken.

avi13:04:57

This might be a little silly, but in some cases I’ve written functions that catch Exceptions and then return them as values! e.g.

(s/fdef check-render-result
  :args (s/cat :result (s/or :success ::r/success-result
                             :failure ::r/failure-result)
               :path   ::fs/file-path-str)
  :ret  (s/or :success nil?
              :failure (partial instance? Exception)))

Alex Miller (Clojure team)14:04:25

function specs document non-exceptional use

Alex Miller (Clojure team)14:04:47

so they don't include any way to talk about exceptions

vlaaad14:04:12

understood

jmromrell19:04:00

I am trying to spec the requests and responses to an API, but am running into issues with namespace collisions.

(ns my-app.validate.endpoint-a)

(s/def :ok-response/status #{200})
(s/def :ok-response/body ...)

(s/def ::ok-response (s/keys :req-un [:ok-response/status
                                      :ok-response/body]))

(s/def :created-response/status #{201})
(s/def :created-response/body ...)

(s/def ::created-response (s/keys :req-un [:created-response/status
                                           :created-response/body]))

(s/def ::response (s/or :ok ::ok-response
                        :created ::created-response))
The problem comes when I add validation for endpoint-b which will also have an :ok-response/body which will be different than that for endpoint-a. I am already encoding the type of response (`ok-response` vs. created-response ) in the kw namespace due to s/keys mandating that the kw name matches the key in the map being spec'd. I can continue to encode further dimensions (endpoint, method) into the namespace to avoid these collisions, but it quickly gets unwieldy:
(s/def :get-endpoint-a-ok-response/body ...)
Am I overlooking an idiomatic way of handling this problem? I'd love to see something like
(s/def ::ok-response-body ...)

(s/def ::ok-response (s/named-keys :req {:status #{200}
                                         :body   ::ok-response-body}))

jmromrell19:04:25

Note: s/named-keys above could also address support for string keys https://clojure.atlassian.net/browse/CLJ-2196

seancorfield19:04:01

Spec 2 will provide that functionality @jmromrell if I’m understanding correctly what you are asking.

jmromrell19:04:25

That's great. The inflexibility of s/keys has been a pain point for me multiple times.

seancorfield19:04:26

But it sounds like you might also want to look at multi-spec?

jmromrell19:04:38

I'll look into it. Thank you!