My team is currently in discussions for how to move forward with structuring our specs, and we've identified something we feel is a limitation with the current implementation.
I fully expect these issues to have a clear solution that is thought through by the spec developers, I'm just not sure what it is yet. Please, go easy.
The main problem we've run into is when a single namespace has code that works with different maps of data that have identical unqualified keys, but different underlying specs. We feel this is a relatively common problem.
Namespace – Generally matches the file where the code is actually located at.
foo.bar.test is located at
Specspace – (my definition) – The namespace portion of a qualified keyword, such as the spec keyword identifier, which may or may not be associated with an actual namespace. Example
foo.core as specspace. As spec is a global registry, this does not need to match a namespace, and I'll refer to this as a "logical specspace", when it denotes a logical grouping rather than a namespace. One key aspect is that this piece is dropped when specified as unqualified in certain specs like
:foo.core/type will match to a keyword
:type rather than requiring the data map has a specspaced
Qualified keyword – keyword with a specspace, e.g.
:foo.core/type. The spec space could be either a namespace or logical.
Unqualified keyword – A bare keyword such as
Advantages of using Namespace as Specspace:
You get to use the
:: shortcut. If you use
::type in the
foo.core namespace, it automatically expands to
:foo.core/type. This also works for alias, so if you require
[project.alice :as a], then you can reference a
::type spec defined inside that namespace as
::a/type. This makes navigating to the code easy, and enforces a clear path between the usage of a spec and it's definition. It also works to prevent accidentally redefining a spec twice, especially in a larger codebase, where a logical specspace.
Limitations of using Namespace as Specspace:
Certain functions in spec, like the
req-un directive of the
keys spec, derive valid keywords by dropping the specspace. This means that you cannot have specs in a single namespace that describe two maps that contain the same key. If there are two map types in a given namespace, and one map uses
:type as a
string? and another map uses
:type as an
integer?, for instance.
Solutions within current spec landscape:
Mix and Match
This lets you use the shortcuts while also being able to define the same unqualified keyword twice in a namespace via logical specspaces, but I feel this has a huge cost of confusing whether the specspace is navigable and what exactly it is referencing. The logical specspaces would not show up in a namespace's require statement, but still would require that the namespace containing the logical specspaces is required, effectively using specs as hidden globals, which can break tooling. The namespace required would look like it isn't being used, and there would be no link between the usage of the spec and knowing where it came from…. obviously not ideal.
Only use namespace specspaces
This means you can never two specs for the same unqualified keyword within the same namespace, which means you need to split into new namespaces anytime this happens. The downside is that it enforces an abstraction and file-level separation that is arguably not logically necessary and hurts locality / readability.
Only use logical specspaces.
This would allow you to keep your spec definitions local to the code that uses them. The downsides are numerous: You lose navigability and the relationship between spec usage and definition, effectively using specs as globals. You lose the ability to use the
:: shortcut for the current namespace, though using it with aliases could be worked around with explicit alias such as
(alias 'logical-specspace 'shortcut), though that functionality isn't there yet and aliases require real namespaces. This also increases the risk of overwriting a spec on a large project if you have a logical spec space collision, if you keep specs local with their code rather than in one big spec file.
Possible solutions requiring development:
Allow a logical component of specspace in addition to namespace.
Example: A spec
::/bar/type declared in the
foo.core namespace that could be referenced as
This would allow us to enforce namespaces for all specspaces, while also allowing us to have multiple specs for a given unqualified keyword within a single namespace.
Rewrite any spec functions that derive an unqualified keyword by dropping specspace to allow aliasing.
This would separate the spec name from the expected key inside the checked data. Ideally, this would be paired with forcing all specspaces to be namespaces, and separates the name of the spec from the key it is checking, possibly causing confusion.
(s/keys :req-un [:foo.core/second-type]) would become `(s/keys :req-un [[:foo.core/second-type :as :type]]
This allows us to define multiple specs sharing an unqualified keyword in a given namespace, at the expense of requiring a name like
third-type that is mapped backed to
type when used.
Allow the definition of a spec to define it's own unqualified keyword.
(s/def ::second-type keyword? :as :type)
This would let you specify what unqualified keys are valid in a given map, while still allowing the spec to be named uniquely. This might not be compatible with spec and is in many ways just a way of adding an extra logical component to the specspace, just handled in extra data rather than the name.