Dynamically Typed Languages Are Not What You Think
Judging by the questions I’ve read about both Swift and Julia in the past, it seems clear to me that a lot of people don’t really have firm grasp of what differentiate a dynamically typed programming language from a statically typed one.
Consider the two lines of code below, defining a function called add
. Each one is from a different programming language. Which line belongs to a statically typed language and which one to a dynamically typed?
add x y = x + y
add(x::Int64, y::Int64) = x + y
The first line is valid Haskell syntax. Haskell is a static and strongly typed language. Yet you can’t see any types mentioned on that line.
The second line is valid Julia code, a dynamically typed language. Yet you can clearly see type information.
What is going on here?
The key thing is that whether a language is strongly, weak, static or dynamically typed has little to do with what the source code looks like. It is all about semantics. How are these lines of code treated in each respective language?
The Haskell Case
Consider what happens if you got these two lines in Haskell
add 1 4
add 2 'c'
The compiler will check the types of the arguments, and okay the first one, while the second line will produce the error message:
No instance for (Num Char) arising from a use of ‘add’
In the expression: add 2 'c'
In an equation for ‘it’: it = add 2 'c'
So I actually never get to run the program. This is because through the usage of the plus operator inside my add
function, the Haskell compiler deduced that my add
function should have the signature:
add :: Num a => a -> a -> a
Now I am not very well versed in Haskell, but this means roughly that we got some type a
which must be some kind of number (Num
). This function takes two arguments of type a
and returns a value of type a
. The reason it is written so oddly is because all Haskell functions are what we call curried. Say we had defined the signature of our function as:
add :: Int -> Int -> Int
You can read this as:
add :: Int -> (Int -> Int)
Where Int -> Int
means a function which takes and integer argument and returns an integer. So essentially add
is defined as:
A function which takes an integer and returns a function which takes and integer, which returns an integer.
Okay that was a bit of a detour about some details of Haskell. It is not my intention to teach you Haskell. The reason I picked Haskell is due to its sophisticated type inference, you can frequently get away with not explicitly stating types. That underscores the point well that static typing is not about whether you see types written or not.
The Julia Case
In a dynamic language using a just in time compiler (JIT) it is a bit harder to separate compile time and runtime. Code gets compiled to machine code on the fly while the program is running.
However unlike a statically typed language, there is no distinct compilation phase where type errors are caught.
If I run a program containing the line:
add(2, 'c')
Julia will not tell me in advance that there is a type mistake in my program and refuse to run it as Haskell would. Instead it will happily run my program until it hits this line at which point it outputs the error message:
ERROR: MethodError: no method matching add(::Int64, ::Char)
Closest candidates are:
add(::Int64, ::Int64) at samplecode.jl:1
As you can see in the picture below, it is helpful and highlights the argument which could not be matched up.
There are several points to observe when comparing with Haskell.
Both languages are in fact strongly typed. Neither Haskell nor Julia actually let you use types in the wrong way.
While static typing prevented us from ever running a faulty program like this, it does this at the expense of complexity of some form. C/C++ and Java e.g. do this by making code more verbose. There is more ceremony, demanding that you specify accurate type information for most code.
Languages such as Haskell avoid the verboseness at the expense of a complex type system. We see this reflected in the error messages. Even for a simple mistake the Haskell error message is tricky to deduce for the novice. While I would argue the Julia error message is more straight forward.
This is a result of a much simpler type system. Users of most dynamic languages do not need to grasp complex type theory.
Conclusion
Type information in static and dynamic languages are used different. In static languages they are used to prevent the compilation of programs containing expressions where types don’t match up. You can’t call functions with types they don’t allow.
In dynamic languages type information is used to select the correct function or method implementation at runtime. In a single dispatch languages such as Ruby or Python this means that we are essentially only specifying he type of the first argument.
Let me demonstrate with an example from Python. I am making two classes with a speak()
method.
class Dog:
def speak(self):
return "bark"
class Cat:
def speak(self):
return "meow"
In my Python REPL I write:
>>> dog = Dog()
>>> cat = Cat()
>>> dog.speak()
'bark'
>>> cat.speak()
'meow'
>>> "animal".speak()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'speak'
'str' object has no attribute 'speak'
The point to notice is that I essentially specified the types of the first arguments to speak()
. In Julia syntax it would look like:
speak(animal::Cat) = "meow"
speak(animal::Dog) = "bark"
In both Julia and Python we are preventing the speak()
from being called for any types has not registered a method for. As you can see "animal".speak()
fails in Python, just as speak("animal")
would have failed in Julia.
So whether we are dealing with Python, Julia or another dynamic language, type information is used to select the correct method at runtime, not to enforce type correctness at compile time.