Which means you really, really, really need a good automated unit test suite. When you switch to a dynamically-typed language, you trade compile-time type guarantees for the ability to ask primitives and objects what type they are at runtime. So, you have to incorporate those questions into your unit tests. Especially when you are doing casting--which of course you should do only when there isn't a better option.
As an aside, static typing without type inference (a la Java) is just a pain. The compiler is just smart enough to enforce typing, but not smart enough to figure out what type anything is unless you explicitly tell it. This puts an extra burden on the programmer.
Fortunately Scala has type inference, so that's an option if you want to use a statically typed functional language on the JVM.
> When you switch to a dynamically-typed language, you trade compile-time type guarantees for the ability to ask primitives and objects what type they are at runtime.
This doesn't strike me as true. In a statically-typed language like C# or Java I can ask objects (and in C#, primitives) what they are all day long. Could you expand what you mean a bit?
(I also don't find Java particularly onerous regarding its lack of type inference--I mean, Java is onerous, just not because of that--and I find that I don't really use type inference much in C++ or C# either. But that is more of a question of taste.)
"In a statically-typed language like C# or Java I can ask objects (and in C#, primitives) what they are all day long."
Which shows that Java and C# are a hybrid of static and dynamic features. Using reflection and introspection to invoke behavior at run time is dynamic-language behavior. More strongly typed languages such as Haskell won't allow you to do this, as far as I know.
I mean that in situations where you have a possible type error, you can substitute compiler typechecking with your own typechecking by using something like (cond (= (type foo) bar)) or (cond (= (class foo) bar)). And you can decide how you want to handle it, instead of necessarily throwing a type error. Plus, polymorphic functions allow you to handle different types with the same function instead of having to overload it. And the test suite lets you run tests like (is (instance? Bar foo)) to help you catch type errors before you push to production.
So, what I mean is, although giving up the type-checking compiler opens up the possibility of introducing runtime type errors in production, it's far from a given that they are going to happen, because dynamic languages give you plenty of other tools to prevent them.
And Java's type system is onerous because it gives you all of the rigidity of static typing but none of the power of type inference. You really notice the difference when you switch to a language like ML or Scala that allows pattern-matching and polymorphic functions, unlike Java where you have to use overloading.
As an aside, static typing without type inference (a la Java) is just a pain. The compiler is just smart enough to enforce typing, but not smart enough to figure out what type anything is unless you explicitly tell it. This puts an extra burden on the programmer.
Fortunately Scala has type inference, so that's an option if you want to use a statically typed functional language on the JVM.