3 ms·
"Without a static type system I would have simply lost a handle on the complexity of the project." A hardcore Clojure developer will probably tell you that Spe
by lkrubner 8y ago
"Without a static type system I would have simply lost a handle on the complexity of the project."
A hardcore Clojure developer will probably tell you that Spec is, in some sense, even better than having a static type system. I haven't used Spec, so I can not say. But I do find, when I'm writing Clojure code, I write pre-conditions on every function, so every function has a contract. This goes a bit beyond what you can get from static type checking, in the sense that I'm testing all of my assumptions, including key names in maps, or at least, this is less work.
There is also the advantage of testing against real world data, instead of mock data. I've had the problem, when using Java, that all the unit tests passed, but the app died in the real world, because the real world data (perhaps from a 3rd party API) was different from our mock data. In such a case you need run time contracts to help find the problem, and static types offer no help (depends on the project really, but the more your app interacts with 3rd parties, the less useful static types are).
I'm not proposing this as The One True Way, but I would propose this as the Really Easy Way to write sane code. I don't have to think about it. It's like writing a comment for the function, but it gets enforced and can not possibly go out of date without giving an error. It documents everything the function is expecting.
An example:
(defn push-sentence
[current-database-connection permanent-id map-of-sentence]
{:pre [
(not (nil? permanent-id))
(not (nil? map-of-sentence))
(map? map-of-sentence)
(:sentence map-of-sentence)
(:belongs-to map-of-sentence)
]}
(mc/update
current-database-connection
"reports"
{:permanent-id permanent-id}
{operators/$push {:sentences map-of-sentence}}
{:upsert true}))
(defn push-transcript-processed
[current-database-connection transcript]
{:pre [
(not (nil? transcript))
(map? transcript)
(:permanent-id transcript)
]}
(mc/update
current-database-connection
"reports"
{:permanent-id (:permanent-id transcript)}
{operators/$set {:processed :true}}
{:upsert true}))
This also gives me good error messages which makes debugging easy. For instance, if this fails:
(not (nil? map-of-sentence))
I get an error about it being nil, whereas if this fails:
(:belongs-to map-of-sentence)
I get an error about the missing key. This helps me narrow down the errors very fast.
I keep the functions short, which feels natural in Clojure.
Again, this isnt' the One True Way. Realy Clojure programmers will tell you to use Spec, or maybe Typed Clojure.
But I will say, heavy use of pre-conditions gives me the right balance between the contract enforcement I want, while still giving me the flexibility of a dynamic language. I like Java, but often the verbose quality of a static typed language is just not something I need.