Fork me on GitHub
#clojure-spec
<
2021-11-22
>
Andrew Byala04:11:56

Hi, everyone. I have a beginner question about Spec's namespacing as it pertains to maps. I'd like to create two definitions within the namespace, such that the following passes:

(s/conform ::account     {:account-id "abc123",   :amount 50})
(s/conform ::transaction {:from-account "abc123", :to-account "def456", :amount 25})
Note that both definitions reference the same non-namespaced key of amount, but I'd like to define the amount of an account to be an int between -100 and 100, and the amount of a transaction to be any positive int. Furthermore, I'd like to use the same spec for the account-id on an account, as well as both the from-account and to-account keys in a transaction. I can't see any way within a map to say that the value of a particular key corresponds to a specification by name. I'm trying to find a way to do something like this, but can't see how:
; This is not working code... I'm trying to find a way to get here.
(s/def ::transaction-amount pos-int?)
(s/def ::balance-amount #(<= -100 % 100))
(s/def ::account-id string?)

; Both :from-account and :to-account should be mapped to the spec for ::account-id
(s/def ::transaction (s/keys :req-un [[:from-account :as ::account-id]
                                      [:to-account :as ::account-id]
                                      [:amount :as ::transaction-amount]]))
; The :amount here is a ::balance-amount, but the :amount for a transaction is a ::transaction-amount
(s/def ::account (s/keys :req-un [::account-id
                                  [:amount :as ::balance-amount]]))

seancorfield04:11:38

(s/def :account/amount  ...)
(s/def :transaction/amount ...)
Then you can use those qualified names in the :req-un part and it will match :account in both cases, with different semantics.

seancorfield04:11:02

Although a lot of examples use :: that's just shorthand for a qualified name that "happens to match" the current namespace -- but qualified keywords should really really reflect their domain semantics -- they don't need to match code namespaces.

seancorfield04:11:04

With the account IDs, you can define a common ::account-id and then just define the other names as aliases:

(s/def ::account-id ..)
(s/def ::from-account ::account-id)
(s/def ::to-account ::account-id)
And, again, as above you can use qualified names that match the domain semantics rather than :: for the code namespace.

Andrew Byala04:11:38

Nifty, @seancorfield. This seems to work for everything I'm looking for:

; This is reused for both transactions and accounts.
(s/def ::account-id string?)

(s/def :transaction/from-account ::account-id)
(s/def :transaction/to-account ::account-id)
(s/def :transaction/amount pos-int?)
(s/def ::transaction (s/keys :req-un [:transaction/from-account :transaction/to-account :transaction/amount]))

(s/def :account/account-id ::account-id)
(s/def :account/amount #(<= -100 % 100))
(s/def ::account (s/keys :req-un [:account/account-id :account/amount]))

1
Andrew Byala04:11:45

Is that what you were suggesting?

seancorfield04:11:16

That looks great, yes!

seancorfield04:11:53

And now you can see how the qualified keys provide separate semantic "namespaces" rather than code namespaces.

1
Andrew Byala04:11:49

Yep, I see that. The piece I was missing was how :req-un would strip that semantic namespace back off again, letting the actual key be whatever I wanted, Super helpful!

Andrew Byala04:11:14

Separate question -- do you find you tend to create a separate namespace just to hold specs for other namespaces, especially if they're shared? Or do you tend to put specs within the namespaces containing other logic?

seancorfield04:11:31

Specs for data structures tend to live in their own namespace, no code. Specs for functions live in the same namespace as the function. Roughly.

seancorfield04:11:09

I wrote about our use of Spec here https://corfield.org/blog/2019/09/13/using-spec/ based on what we do at World Singles Networks.

thanks3 1
vemv09:11:50

Has it been hammocked (officially or not) to give the notion of coverage to specs? e.g. for an (s/or), verify at runtime, integrated with a given test suite, that all branches of the or have been exercised. (I know that generative testing is a thing! I want it the other way around here: start by code and reach the spec, not vice versa)

vemv09:11:21

One big use case, is that for any defn that returns s/or :result ,,, :error ,,,I should rest assured that the test suite has coverage for the green and red paths of the spec alike

Alex Miller (Clojure team)13:11:13

Haven't talked about it

Colin P. Hill13:11:20

Is there a writeup anywhere about the reason that instrument doesn't validate :ret and :fn? I was wondering if this was considered and decided against, or there were some non-obvious difficulties. There's a library out there that does this, but I've got this nagging suspicion that it was left out of the first-party spec library for a reason.

Colin P. Hill14:11:24

Ah my bad, should have looked around for a FAQ, of course it's on there. Thank you!