practicalli / practicalli/clojure

clojure.spec a function with optional arguments

Open
#123 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

spec
Dominant language
Makefile
Stars
117
Forks
36
PR merge metrics
No merged PRs in 30d

Description

spec a function with optional arguments
specifically use (st/instrument) in a test as suggested and the following code breaks with the following exceptions:(ship (:id p)) ; (:id p) is surely a uuid|| :cause "class java.util.UUID cannot be cast to class java.lang.Number (java.util.UUID and java.lang.Number are in module java.base of loader 'bootstrap')"
Untitled

(s/def ::id uuid?)
(s/def ::src-planet-id uuid?)
(s/def ::dest-planet-id uuid?)
(s/def ::speed #(> % 0))
(s/def ::departure-time #(or (nil? %) (instance? DateTime %)))

the exception is invoked on the s/def of ::speed 😕

what is (random-id) ?
It's my own function, which calls (UUID/randomUUID) wth java.util.UUID

what about default-ship-speed?
(defonce default-ship-speed 20)

What is DateTime?
(:import [org.joda.time DateTime]
[java.util UUID]))

Though I use clj-time, so I might be wrong here when I think of it (edited)
Joda Time is deprecated. If you're on Java 8 or later, you should use Java Time really.
clj-time is also deprecated.

(s/def ::speed (fn [v] (and (int? v) (> v 0))))
this definition works
but dunno why it assumes uuid there by default when doing coercion (edited)

The problem is that all three optional arguments are optional independently
So dest-planet-id could be omitted and the speed and departure-time could still be provided.
In other words, it tries to check (ship src-planet-id src-planet-id) against a signature that is essentially [uuid? #(> % 0)] and that's causing the exception.

And that's why adding (and (int? %) ...) into ::speed "works" -- it guards the > operation from being called on non-numeric values.

Because then the ::speed spec successfully fails to match (instead of blowing up) and Spec goes on to try the other options.

I'd suggest spec'ing ship a bit differently, perhaps using s/alt over the four arities, each spec'd with no optional parameters.
Or at least across the first three and leave just :departure-time as s/?

Thanks a lot! However I don't fully understand how this happens:

dest-planet-id could be omitted and the speed and departure-time could still be provided.

You have src dest? speed? time? -- each of those three are optional, so each could be omitted while the others are passed.
so src speed is "valid", as is src time or src dest time or src speed time.
That's what your fdef says.
So when Spec sees (ship src-planet-id src-planet-id) it's going to try src speed, src time, and src dest in some random order.
Since your speed spec just tries to compare the value > it will throw an exception if passed a non-number: which a UUID is.
Because Spec encounters an exception, it won't try the other options -- it just propagates the exception.

-> clj -A:test -Sdeps '{:deps {clj-time {:mvn/version "RELEASE"}}}'
Clojure 1.10.1
user=> (require '[clojure.spec.alpha :as s] '[clojure.spec.test.alpha :as st])
nil
user=> (import '(org.joda.time DateTime) '(java.util UUID))
java.util.UUID
user=> (defonce default-ship-speed 20)
#'user/default-ship-speed
user=> (defn random-id [] (UUID/randomUUID))
#'user/random-id
user=> (s/def ::id uuid?)
:user/id
user=> (s/def ::src-planet-id uuid?)
:user/src-planet-id
user=> (s/def ::dest-planet-id uuid?)
:user/dest-planet-id
user=> (s/def ::speed #(> % 0))
:user/speed
user=> (s/def ::departure-time #(or (nil? %) (instance? DateTime %)))
:user/departure-time
user=> (s/def ::resources #(>= % 0))
:user/resources
user=> (s/def ::ship (s/keys :req-un [::id ::src-planet-id ::dest-planet-id ::speed ::departure-time ::resources]))
:user/ship
user=> (defrecord Ship [id src-planet-id dest-planet-id speed departure-time resources])
user.Ship
user=> (s/fdef ship
        :args (s/alt :arity1 (s/cat :src-planet-id ::src-planet-id)
                     :arity2 (s/cat :src-planet-id ::src-planet-id :dest-planet-id ::dest-planet-id)
                     :arityN (s/cat :src-planet-id ::src-planet-id :dest-planet-id ::dest-planet-id :ship-speed ::speed :departure-time (s/? ::departure-time)))
        :ret ::ship)
user/ship
user=> (defn ship
  ([src-planet-id]
   (ship src-planet-id src-planet-id))  ([src-planet-id dest-planet-id]
   (ship src-planet-id dest-planet-id default-ship-speed))  ([src-planet-id dest-planet-id ship-speed]
   (ship src-planet-id dest-planet-id ship-speed nil))  ([src-planet-id dest-planet-id ship-speed departure-time]
   (->Ship (random-id) src-planet-id dest-planet-id (if (nil? ship-speed) default-ship-speed ship-speed) departure-time 0)))
#'user/ship
user=> (st/instrument)
[user/ship]
user=> (ship (random-id))
#user.Ship{:id #uuid "3ad92050-2b9b-499e-b7e3-f933053d7b1c", :src-planet-id #uuid "79cd0367-d78d-4774-9f5f-3a29a8c77851", :dest-planet-id #uuid "79cd0367-d78d-4774-9f5f-3a29a8c77851", :speed 20, :departure-time nil, :resources 0}
user=>

^ That shows the s/alt structure that would work

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the supplied Clojure REPL reproduction, especially the s/fdef for ship and the call after (st/instrument). Check how the optional argument specs are matched and compare the behavior with the shown s/alt structure; done means valid optional-argument calls no longer throw while invalid calls fail as spec errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
clojure
Domain
testing-qa
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.