Static and Dynamic
The programming experience is significantly impacted by when a language's tooling considers types. Type systems fall into two broad categories: static type systems and dynamic type systems.
Static Type Systems
In a statically-typed language, the compiler performs typechecking at compile time, which means type errors are discovered before an executable is built. In addition to checking types, the compiler uses the type information to figure out what code to jump to on a method call or what machine instruction to invoke. Making these decisions about control flow at compile time means they do not to be made at runtime, leading to faster execution.
The executable that the compiler builds might not contain any type information whatsoever. Consider this C program:
int twice(int x) {
return x + x;
}
The body of the function compiles down to this x86 assembly:
movl %edi, %eax # copy parameter, %eax = %edi
addl %edi, %eax # %eax += %edi
You may not be familiar with x86. The important point is that types are absent from this code. The compiler considered the type of x
and the return type when choosing the addl
instruction, but all other type information has been stripped away. This code will run blazingly fast because it's not asking any questions about the types of the data.
Types aren't always completely removed from the compiled code. Several otherwise statically-typed languages provide mechanisms for programmers to perform their own typechecks at runtime. Java and JavaScript provide instanceof
, Ruby is_a?
and instance_of?
, Python type
and isinstance
, and C++ dynamic_cast
and typeid
. If we are writing a polymorphic function and really need to know what the underlying type of a value is, we can query it in these languages.
Dynamic Type Systems
In a dynamically-typed language, the interpreter performs typechecking at runtime, just before an operation is performed. Our program may be riddled with type errors, but the interpreter will still run the code up until the first error. If an operation is not in the flow of execution, it will not be typechecked. Consider this Ruby program, whose then-block contains an illegal operation:
if false
7.remove()
else
puts 'Respect was invented to cover the empty place where love should be.'
end
You can't remove 7. That's a type error. However, this script runs just fine because the then-block is not executed.
In most dynamically-typed languages, variables are not locked in to a single type. For example, the variable number
holds values of two different types in this Ruby program:
number = 7 # Integer
number = number.to_f # Float
Therefore, in dynamically-typed languages, types are a property of values, not of variables. 7 and the value returned by to_f
have a fixed type, but number
does not.
Dynamic typing is implemented in two different ways: through type tags or through reflection. A type tag is metadata that is stored with a value that identifies the value's type. For example, this universal Box
class pairs a value with a type tag:
class Box
def initialize(type, value)
@type = type
@value = value
end
end
The type tag is queried to determine which operation to invoke. For example, this function uses the tag to arbitrate between an integer operation and a string operation:
def rightmost(box)
if value.tag == :int
box.value % 10
elsif value.tag == :string
box.value[-1]
end
end
If the tag reports an integer, we extract the rightmost digit. If the tag reports a string, we extract the rightmost character.
With reflection, the interpreter does not ask the value about its type at all. Rather, the interpreter asks the value about the operations it supports. In Ruby, we ask a value if it supports an operation using the respond_to?
method. The rightmost
method may be rewritten using this reflection method:
def rightmost(value)
if value.respond_to? :%
value % 10
elsif value.respond_to? :[]
value[-1]
end
end
The advantage of reflection is that rightmost
now works for any values that support the %
or []
operators, not just int
and string
. The type is irrelevant. Dynamic typing through reflection is called duck typing. Instead of asking an animal if it is a duck, we ask if it quacks, swims, or waddles. We don't care if it's a duck.
False Dichotomy
Programmers argue about whether static or dynamic typing is better. Both are capable of bringing joy and inflicting pain. When someone ranks them, they are telling you more about what kind of software they write and less about a universal truth. In fact, there's no need to pick just one of these systems. Pick them both. Use a dynamically-typed language when you want to get code up and running quickly. Use a statically-typed language when you want to build fast executables.
Also, statically-typed languages are not always completely statically-typed. Consider this Java program that creates an array of strings, makes an alias of the array reference of type Object[]
, and then attempts to update element 0 through the alias. Does this code typecheck, compile, and execute?
String[] elements = {"tungsten", "tin"};
Object[] items = elements;
items[0] = "molybdenum";
System.out.printf("%s %s%n", items[0], items[1]);
It does all three. After all, items
is an Object[]
. A string is an Object
, so assigning it to element 0 poses no problem. But what happens if you change the assignment to something that's not a string?
String[] elements = {"tungsten", "tin"};
Object[] items = elements;
items[0] = new Double(-1);
System.out.printf("%s %s%n", items[0], items[1]);
This code compiles just fine, which means it passes the static typechecking tests. However, behind the alias items
is an array of strings, and storing a Double
in it should fail. Indeed, when you run the program, you encounter an ArrayStoreException
. The Java designers identified that their design of the language made arrays vulnerable to type mismatches that could not be detected statically. So they added a dynamic typecheck that happens every time you assign a value to an array.