Where to put specs for Clojure.Spec?
Asked Answered
J

3

52

So, I'm diving deeper and deeper into Clojure.Spec.

One thing I stumbled upon is, where to put my specs. I see three options:

Global Spec File

In most examples, I found online, there is one big spec.clj file, that gets required in the main namespace. It has all the (s/def) and (s/fdef) for all the "data types" and functions.

Pro:

  • One file to rule them all

Contra:

  • This file can be big
  • Single Responsibliy Principle violated?

Specs in production namespaces

You could put your (s/def) and (s/fdef) right next to your production code. So that, the implementation and the spec co-exist in the same namespace.

Pro:

  • co-location of implementation and spec
  • one namespace - one concern?

Contra:

  • production code could get messy
  • one namespace - two concerns?

Dedicated spec namespace structure

Then I thought, maybe Specs are a third kind of code (next to production and test). So maybe they deserve their own structure of namespaces, like this:

├─ src
│  └─ package
│     ├─ a.clj
│     └─ b.clj
├─ test
│  └─ package
│     ├─ a_test.clj
│     └─ b_test.clj
└─ spec
   └─ package
      ├─ a_spec.clj
      └─ b_spec.clj

Pro:

  • dedicated (but related) namespaces for specs

Contra:

  • you have to source and require the correct namespaces

Who has experience with one of the approaches?
Is there another option?
What do you think about the different options?

Jennajenne answered 21/6, 2016 at 10:54 Comment(0)
T
22

I usually put specs in their own namespace, alongside the namespace that they are describing. It doesn't particularly matter what they're named, as long as they use some consistent naming convention. For example, if my code is in my.app.foo, I'll put specs in my.app.foo.specs.

It is preferable for spec key names to be in the namespace of the code, however, not the namespace of the spec. This is still easy to do by using a namespace alias on the keyword:

(ns my.app.foo.specs
  (:require [my.app.foo :as f]))

(s/def ::f/name string?)

I'd stay away from trying to put everything in one giant spec namespace (what a nightmare.) While I certainly could put them right alongside the spec'd code in the same file, that hurts readability IMO.

You could put all your spec namespaces in a separate source path, but there's no real benefit to doing so unless you're in a situation where you want to distribute the code but not the specs or vice versa... hard to imagine what that'd be though.

Toddy answered 21/6, 2016 at 13:27 Comment(9)
What would be your advice for using specs for destructuring when they're in a separate namespace?Yaws
@Toddy Sam Estep made a good point in his question that you can't rely on a spec being loaded to use it for parsing because you can't require the spec ns if it already requires your ns. So probably a (load "my/app/foo_specs")/(in-ns 'my.app.foo)(require '[clojure.spec :as s]) with no seperate ns would be a preferable pattern if you want specs in a seperate file for readabilityBurchfield
That's a good point... There are situations where you want your implementation namespace to depend upon your spec namespace, which implies that your spec ns cannot depend on the impl ns. Currently, there's no good way to resolve this - the best would be just to type out the fully qualified namespace names in the spec. Fortunately, there are some changes in the works that will allow you to alias namespaces that haven't been loaded/required yet, which should solve this problem neatly.Toddy
@Toddy But what about my second example, in which the spec for factor uses the prime? function that was defined earlier in the same namespace? It just doesn't seem like having the implementation namespace require the spec namespace (or vice versa) can work in general.Yaws
@SamEstep yeah, you get to choose which direction to have a dependency. In your scenario, where your spec logically depends on your imply ns, you can either just put it all in the same namespace or factor prime? out to a separate common dependency. I understand what you're trying to do, but I think it's important to recognize that you've set up a sort of inherent circular dependency, the way you've stated the problem. You want your impl to depend on a spec which in turn depends on the impl. The only solution is to split and have either two specs or two implementations.Toddy
@Toddy No, that's not the only solution. As I said in my question, putting the specs and functions in the same namespace solves all those issues very neatly.Yaws
@SamEstep Yeah clearly that works. But it works because you can define things in an interleaved order. My point is that you must define spec in two "places" in relation to the impl code, whether that's before and after in the same ns or splitting into different nses.Toddy
I agree there's a readability cost to specs in the same file, but there's a different, maybe worse readability cost to defining things in a namespace outside of the main file for that namespace. Names become magically available without any require or use in the main file. A standardized practice of doing this all the time with specs is OK, but an outsider looking at your code could still be confused. (That outsider might be you a year or two later.) Prominent warning comments seem needed. The beautiful thing about normal use of the namespace system is that such comments aren't needed.Jerejereld
@levand: "Fortunately, there are some changes in the works that will allow you to alias namespaces that haven't been loaded/required yet" Please tell more.Ewen
F
17

In my opinion the specs should be in the same ns as the code.

My main consideration is my personal philosophy, and technically it works out well. My philosophy is that the spec of a function is an integral part of it. It isn't an extra thing. It's definitely not "two concerns" as put in the OP. Actually, it's what the function is, because in terms of correctness: who cares about the implementation? who cares what you wrote in your defn? who cares about the identifier? who cares about anything but the spec?
I think it's weird because not only clojure.spec came later, also most languages will not let you have specs as an integral thing even if you wanted it to be, and anything close (tests in the code perhaps) is usually frowned upon, so of course it seems weird. But give it some thought, and you might reach a conclusion like I did (or you may not, this part is opinionated).

The only good reasons I can think of as to why you wouldn't want specs in the same ns are these two reasons:

  1. It clutters your code.
  2. You want to support Clojure versions pre Clojure 1.9.0.

As for the first reason, well, again, I think it's an integral part of your functions. If you find that it really is too much, my advice would be the same as if your ns would be too cluttered up regardless of spec: see if you can split it up.

As for the second reason, if you truly care, you can check in code if the clojure.spec ns is available, if not then shadow the names with functions/macros that are NOP. Another option is to use clojure-future-spec but I haven't tried it myself so I don't know how well it works.

Another way this works out well technically is that sometimes there's a cyclic dependency that you cannot handle with different namespaces, e.g. when your specs depend on your code (for function specs) and your code depends on your specs for parsing (see this question).

Farfetched answered 4/1, 2017 at 10:43 Comment(2)
I think I agree with this. The rationale article on spec mentions documentation as one aspect of them. If you put them somewhere far from the code they spec, you've lost that benefit.Hephzipa
I prefer this as well, with the notable exception that specs shared between a clojure backend and a clojurescript frontend I tend to put in separate cljc files rather than writing all my namespaces as cljc.Fulllength
C
8

One other consideration depending on your use case - putting the specs alongside your main code limits use of your code to Clojure 1.9 clients, which may or may not be what you want. Like @levand, I would recommend a parallel namespace for each of your code namespaces.

Caricaria answered 21/6, 2016 at 22:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.