I’ve written a lot of code in my career as a software developer thus far, and in a variety of languages – some C early on, a crap-ton of Java, a chunk of Python and Ruby and JavaScript, and a smidgen of C# and other languages. I’m not exactly religious about my language preferences: what works, works – and each has its place.
I’ve always been an adherent of dynamic typing (and not necessarily a church-goer of that particular religion): I guess I just dislike the verbosity and general nitpicky-ness of having to declare upfront the shape and space of the data my code operates on. However, I do think that type coercion is the wrong thing to do – e.g., if given a string and an integer, your programming language should error out when you ask it to add those two things together, not coerce one to the other.
However, I also understand and appreciate some of the safety that compile-time types gives you: having a compiler check that you are indeed returning an int from a function or method does catch those times where you get a brainfart.
That said, I do believe that types are not a panacea, and with at least the type systems that I’ve used, they sometimes risk becoming crutches for building unit and integration tests. Knowing the compiler accepted your code as-is does not say anything about whether or not it’s correct (“it compiled, it must work!”) – and in a lot of cases, the benefits of compile-time static types are sometimes obviated by the fact that you still need to write tests. The bogeyman of type failures in a dynamic language has never really arisen, at least in the codebases I’ve worked on, especially with sufficient test coverage.
One thing that helps is writing in a functional style and avoiding mutation at all costs. If your functions and methods do not mutate state, have no side effects, and if each variable is assigned only once, then the problems of type confusion tends to go away as well. Being able to treat something as a black box with idempotent behavior tends to help in other places too, so it’s something I tend to do anyway; it makes it easier to reason about such stuff, in the absence of types.
Having no support from the language to at least define acceptable inputs tends to add to the cognitive burdern though, and types do have that going for them. I’m a big fan of Clojure (and other Lisps actually), and I’ve been following both Spec and Plumatic (née Prismatic) Schema – Spec and Schema both allow you to describe the structure and shape of your data beyond what most static type systems would allow, as they operate at runtime. When combined with generative tests, having support for declaring the shape of actual inputs and outputs at runtime allows for better coverage and better outcomes, I think.
I keep coming back to compile-time static types, however.
There is a cost to having the above dynamic type systems, and honestly I don’t know whether those costs can be borne out in certain contexts. I can’t really argue for building high performance systems in a dynamically-typed language such as Ruby, for instance, and there’s just too many mistakes that can be made in such languages without the discipline needed to keep things straight.
I’ve not yet really made up my mind entirely. As much as I like dynamic typing and languages such as Clojure and Python, the safety blanket of types does matter. So, maybe someday I’ll pick up a statically-typed language that does it for me. Who knows?
(PS: That language is not Haskell).
Previously: Procrastination