Dynamically-typed languages are uni-typed
Comparing type systems, there's no advantage in dynamic typing. Dynamic typing is a special case of static typing - it's a statically-typed language where every variable has the same type. You could achieve the same thing in Java (minus conciseness) by making every variable be of type Object
, and having "object" values be of type Map<String, Object>
:
void makeItBark(Object dog) {
Map<String, Object> dogMap = (Map<String, Object>) dog;
Runnable bark = (Runnable) dogMap.get("bark");
bark.run();
}
So, even without reflection, you can achieve the same effect in just about any statically-typed language, syntactic convenience aside. You're not getting any additional expressive power; on the contrary, you have less expressive power because in a dynamically typed language, you're denied the ability to restrict variables to certain types.
Making a duck bark in a statically-typed language
Moreover, a good statically-typed language will allow you to write code that works with any type that has a bark
operation. In Haskell, this is a type class:
class Barkable a where
bark :: a -> unit
This expresses the constraint that for some type a
to be considered Barkable, there must exist a bark
function that takes a value of that type and returns nothing.
You can then write generic functions in terms of the Barkable
constraint:
makeItBark :: Barkable a => a -> unit
makeItBark barker = bark (barker)
This says that makeItBark
will work for any type satisfying Barkable
's requirements. This might seem similar to an interface
in Java or C# but it has one big advantage - types don't have to specify up front which type classes they satisfy. I can say that type Duck
is Barkable
at any time, even if Duck
is a third party type I didn't write. In fact, it doesn't matter that the writer of Duck
didn't write a bark
function - I can provide it after-the-fact when I tell the language that Duck
satisfies Barkable
:
instance Barkable Duck where
bark d = quack (punch (d))
makeItBark (aDuck)
This says that Duck
s can bark, and their bark function is implemented by punching the duck before making it quack. With that out of the way, we can call makeItBark
on ducks.
Standard ML
and OCaml
are even more flexible in that you can satisfy the same type class in more than one way. In these languages I can say that integers can be ordered using the conventional ordering and then turn around and say they're also orderable by divisibility (e.g. 10 > 5
because 10 is divisible by 5). In Haskell you can only instantiate a type class once. (This allows Haskell to automatically know that it's ok to call bark
on a duck; in SML or OCaml you have to be explicit about which bark
function you want, because there might be more than one.)
Conciseness
Of course, there's syntactical differences. The Python code you presented is far more concise than the Java equivalent I wrote. In practice, that conciseness is a big part of the allure of dynamically-typed languages. But type inference allows you to write code that's just as concise in statically-typed languages, by relieving you of having to explicitly write the types of every variable. A statically-typed language can also provide native support for dynamic typing, removing the verbosity of all the casting and map manipulations (e.g. C#'s dynamic
).
Correct but ill-typed programs
To be fair, static typing necessarily rules out some programs that are technically correct even though the type checker can't verify it. For example:
if this_variable_is_always_true:
return "some string"
else:
return 6
Most statically-typed languages would reject this if
statement, even though the else branch will never occur. In practice it seems no one makes use of this type of code - anything too clever for the type checker will probably make future maintainers of your code curse you and your next of kin. Case in point, someone successfully translated 4 open source Python projects into Haskell which means they weren't doing anything that a good statically-typed language couldn't compile. What's more, the compiler found a couple of type-related bugs that the unit tests weren't catching.
The strongest argument I've seen for dynamic typing is Lisp's macros, since they allow you to arbitrarily extend the language's syntax. However, Typed Racket is a statically-typed dialect of Lisp that has macros, so it seems static typing and macros are not mutually exclusive, though perhaps harder to implement simultaneously.
Apples and Oranges
Finally, don't forget that there's bigger differences in languages than just their type system. Prior to Java 8, doing any kind of functional programming in Java was practically impossible; a simple lambda would require 4 lines of boilerplate anonymous class code. Java also has no support for collection literals (e.g. [1, 2, 3]
). There can also be differences in the quality and availability of tooling (IDEs, debuggers), libraries, and community support. When someone claimed to be more productive in Python or Ruby than Java, that feature disparity needs to be taken into account. There's a difference between comparing languages with all batteries included, language cores and type systems.