The main disadvantage of dynamic typing for me is the lack of verifiable documentation about what data a function needs, and which data comes out of it. This becomes a problem when
a) the data is complex (e.g. dictionary of lists of items with certain properties)
b) I haven't looked at the function for a while.
In those cases, I find statically typed code easier to reason about.
On the other hand, it absolutely maddens me that static type systems force me to spell everything out, even where it's trivial. It slows me down, it bloats the code (especially when you have to create a new class for everything) - which again makes it harder to see the purpose of the code instantly.
That's why I decided to make my own little pragmatic experiment:
In cases where complexity is expected (or experienced) - and only there- I use the Clojure pre- and post-conditions to check the shape of selected parameters and/or its return value.
It looks like this:
(defn german-holidays
"Returns map of german holidays, in the year of d."
[d]
{:post [(like {(date 1 1 2012) :easter} %)]}
...)
You can instantly see how the data is supposed to look like, it will be checked automatically, and it is close to the actual code where you need it (unlike e.g. Unit Tests).
I'm not yet sure how this experiment will fare in the future, so any suggestions or warnings are appreciated.
I use tests for documentation. They seem like more work at the start, but they're more flexible. You can self-document things with tests that you cannot with just types.
You're right that tests aren't close to code. But that can be good or bad. Since it's not next to the code it can be more verbose and thorough without adding a constant reading burden. And when I wonder, "why is this line here?" I can comment it out and run the tests to find out.
I think of tests as records of the input space of my program. Most of the time we go to great lengths to make explicit what we do but not quite what aspects of its behavior we truly care about. Where are the dont-cares in this program? If I refactor it am I restricted to a bit-for-bit identical behavior? In practice that's impossible, and without tests we fudge random parts of the state space without rigorously knowing what is important and what isn't.
Static type systems don't excuse you from writing tests. But they do effectively write a whole class of tests for you, and run them much faster and give you more precise errors than manual testing ever will.
People complain about C++ compile times but I've heard of big Ruby projects with test suites that take 20+ hours to run.
Ah, but a lot of bugfixes are developed by making small changes to understand better what is going on internally during the bug symptom, and there the compile time is more important than the compile/test time.
> You can self-document things with tests that you cannot with just types
Absolutely.
But I would say that is a different debate. In my case, I don't want to specify the behavior, but only the form, which is the counterpart of a static type, but more flexible (e.g. {} is likened to any structure that behaves like a map - the class doesn't have to be exactly the same).
With my like-function, I don't have to spend too much time specifying what is expected, and I get a lot of bang for the buck.
I want to do just enough so the code stays comprehensible and thus manageable.
(fn parse-int [str] ...) needs no type or unit-test to be understood.
(fn parse-appointment [str] ...) is better understood when it has {:post [(like {:id (UUID.) :name "me" :date (java.util.Date .)} %)]}
And as soon as the function gets really smart, and it's smartness isn't revealed directly by the code, it would be time for either a good comment or a Unit Test (or make the code better so that it does, which is an often forgotten option).
You might want to read up on Design By Contract, if you haven't already. It's an approach to writing robust software by incorporating explicit pre- and post-conditions into code. IIRC they were used to generate documentation for functions as well as to do the run-time checks. It originally came from the Eiffel community I believe, but has spread to other languages as well.
The main disadvantage of dynamic typing for me is the lack of verifiable documentation about what data a function needs, and which data comes out of it. This becomes a problem when
a) the data is complex (e.g. dictionary of lists of items with certain properties)
b) I haven't looked at the function for a while.
In those cases, I find statically typed code easier to reason about.
On the other hand, it absolutely maddens me that static type systems force me to spell everything out, even where it's trivial. It slows me down, it bloats the code (especially when you have to create a new class for everything) - which again makes it harder to see the purpose of the code instantly.
That's why I decided to make my own little pragmatic experiment: In cases where complexity is expected (or experienced) - and only there- I use the Clojure pre- and post-conditions to check the shape of selected parameters and/or its return value. It looks like this:
(defn german-holidays
(defn calendar #{}}} %)]} You can instantly see how the data is supposed to look like, it will be checked automatically, and it is close to the actual code where you need it (unlike e.g. Unit Tests).I'm not yet sure how this experiment will fare in the future, so any suggestions or warnings are appreciated.