>What ends up happening in practice instead is that large projects using dynamic languages are hesitant to change anything like that.
I have seen this "code fear" in both statically and dynamically typed languages and fixed it. When I have worked through it, it has been roughly an equal amount of work to solve it in dynamically and statically typed languages.
Integration tests are usually the key to getting over the code fear hump.
>The equivalent would be to have integration tests that exercise the entire system with all its functions and code paths checking for basic data compatibility.
Integration tests that exercise every relevant user story are a prerequisite for avoiding code fear in any language - statically OR dynamically typed.
In addition to this sprinkling additional type checks around the code helps give confidence that the code is indeed working and gives you additional freedom to refactor. This is something I do when:
A) I encounter a type related error that an assertion would have caught.
B) I was previously confused about what type a var is or if just by looking at it I think somebody else might be confused.
People who have terrible test suites for their code written in statically typed languages are usually insistent that it's best practice to have types checked by the compiler. This means two things about their development practice A) they don't drive development with tests (TDD) and B) they often push unexercised code.
This is a claim I hear often. I don't think its true. Its possible to have integration tests for every user story, yet still not cover even a fraction of the possible code paths due to an explosion of different possible outcomes and inputs.
The great thing about types is that they're guaranteed to cover all code paths. They can't guarantee certain categories of things, but for the things they can guarantee, they easily provide 100% coverage, something that is impossible with integration tests alone.
They are also a tool to greatly reduce the number of allowed inputs and outputs for most code, which in turn greatly reduces the number of tests that actually need to be written. Runtime type assertions only provide one level of that: if you are sending nonsensical input to a method in an uncommon code path not exercised by your integration tests, the runtime type check will probably fail too late (in production). While runtime assertion can tell you if you call your code wrong at runtime, they can't answer "What are all the places that send incorrect input to this method?" Types can do that.
Not to mention that runtime assertions can be costly, unnecessary and very repetitive if added for every method. Comparatively types are free in terms of both performance and programmer effort, especially in languges with good inference.
In short, tests cannot replace types, just like types cannot replace tests. In the areas where they do overlap, types are a way better, more efficient, more tooling friendly choice.
>Its possible to have integration tests for every user story, yet still not cover even a fraction of the possible code paths due to an explosion of different possible outcomes and inputs.
Which is true in any language.
Having a hierarchy of integration tests helps to offset the combinatorial explosion, as does sanity checks that eliminate invalid code paths. These perform a very similar function to static types but you don't have to use them in prototype code and you can add them retrospectively in production code.
>The great thing about types is that they're guaranteed to cover all code paths.
Types are something every programming language has.
>They can't guarantee certain categories of things, but for the things they can guarantee, they easily provide 100% coverage, something that is impossible with integration tests alone.
Inserting type and sanity checks can achieve virtually the same guarantees as static typing when you have a comprehensive test suite.
When you don't have a comprehensive test suite, static typing may seem more attractive but it's a false sense of security.
>if you are sending nonsensical input to a method in an uncommon code path not exercised by your integration tests, the runtime type check will probably fail too late (in production).
Given that you have untested code, you have a higher likelihood of bugs. Period. In statically typed languages too. In dynamic languages those bugs are likely to manifest a bit differently. Yay?
>While runtime assertion can tell you if you call your code wrong at runtime, they can't answer "What are all the places that send incorrect input to this method?" Types can do that.
Arguments that argue best practice that start with with "given that I have poor test coverage..." are not especially convincing.
>Not to mention that runtime assertions can be costly
Runtime assertions are costly in CPU time. Static typing is costly in programmer time. CPUs are cheap, programmers are not.
>unnecessary and very repetitive if added for every method.
Oh yes, which is why I don't add them to every method - just methods where I am convinced they will be useful.
>Comparatively types are free in terms of both performance and programmer effort
Static typing never comes for free and it is often highly inappropriate (which is why many statically typed languages have dynamic typing bolted - e.g. see reflection in Java).
>In short, tests cannot replace types, just like types cannot replace tests.
You were arguing above that static typing was great because it caught bugs in untested code. That certainly sounds like you think it is replacing tests.
> Given that you have untested code, you have a higher likelihood of bugs. Period. In statically typed languages too. In dynamic languages those bugs are likely to manifest a bit differently. Yay?
Yes, test proponents love to say "you are not testing well enough!" Its dishonest, because in practice no system can achieve 100% integration test coverage, so no system is testing well enough :)
> You were arguing above that static typing was great because it caught bugs in untested code. That certainly sounds like you think it is replacing tests.
I never argued that. In fact, I started by saying that what types provide can also be provided by integration tests with that have 100% coverage, but that the second one is a lot more difficult to achieve AND doesn't give the nice extra tooling support (automatic refactoring, extra documentation, instant type incompatibility feedback in IDEs).
Indeed, if you can keep your integration test coverage at 100% in your project, you don't need types. Curious though; are you familiar with an open source project that has that?
Also, 2000 called, they want their static types arguments back. Take a look at modern type systems, especially those that are tailored for dynamic languages (TypeScript, core.typed). They've grown in expressive power and type inference. They've also patched some obviously stupid gaping holes: for example, TypeScript with strictNullChecks will prevent "NPE" bugs.
I have seen this "code fear" in both statically and dynamically typed languages and fixed it. When I have worked through it, it has been roughly an equal amount of work to solve it in dynamically and statically typed languages.
Integration tests are usually the key to getting over the code fear hump.
>The equivalent would be to have integration tests that exercise the entire system with all its functions and code paths checking for basic data compatibility.
Integration tests that exercise every relevant user story are a prerequisite for avoiding code fear in any language - statically OR dynamically typed.
In addition to this sprinkling additional type checks around the code helps give confidence that the code is indeed working and gives you additional freedom to refactor. This is something I do when:
A) I encounter a type related error that an assertion would have caught.
B) I was previously confused about what type a var is or if just by looking at it I think somebody else might be confused.
People who have terrible test suites for their code written in statically typed languages are usually insistent that it's best practice to have types checked by the compiler. This means two things about their development practice A) they don't drive development with tests (TDD) and B) they often push unexercised code.