The Many Advantages of Dynamic Languages
How static type checking force you to learn 6 different languages in the worst case
The advantages of using a dynamically type language goes far beyond saving key strokes to write the type of a variable or argument.
Writing type information is not the key difference between dynamic and static programming languages. There are dynamic languages such as Julia where you frequently write type information:
add(x::Int64, y::Int64) = x + y
Likewise there are statically typed languages such as Haskell where you seldom have to write type information because the types are inferred by the compiler:
add x y = x + y
I explore this apparent contradiction further in this story.
If you experiment with different static and dynamic languages you will struggle to put your finger on exactly what fundamental aspect of dynamic languages make them so much more productive.
I will go through some examples here, which will be a bit exaggerated for the sake of clarity. If one adds too many caveats and nuances, the core differences will become hard to spot for the reader.
The fundamental difference between dynamic and static languages is that in dynamic languages values have types, while variables and expressions don’t. Static languages are the opposite, expressions and variables have types. Values tend to be binary blobs at runtime.
There are exceptions to these key differences, but this is in a nutshell what the difference is.
Downsides of Static Typing
From this simple difference follows a long list of consequences, which in my opinion favors the dynamic languages.
Build System
Because every type in a statically typed language has to be accounted for before a program runs, the compiler needs to ruthlessly hunt down every type a function ever use. No type shall remain unknown. If any type slipped away we would risk complete confusion during runtime when the system tries to make sense of an operation done to two values and realizing it knows absolutely nothing about them except that they are blobs of binary data.
Hence not getting every single type right before running would have undefined and catastrophic results.
For this reason every statically typed language has a large and intricate piece of software called the build system. It starts off easy, but soon developers discovers that describing dependencies in a large software project is tricky and the build system morphs into a fully fledged programming language. Scons is one example:
env = Environment()
env.Append(CPPFLAGS=['-Wall','-g'])
env.Program('hello',
['hello.c', 'main.c'])
You may be a humble C++ developer who thinks all you ever need to learn is trusty old C++. Sorry mate! You also have to learn Python otherwise you cannot build your C++ project. There is an interesting exception to this rule found in the Zig programming language. Zig uses Zig as the build system language.
But for everybody else not using Zig, they are now forced to learn two languages! But wait, we actually have 4 more languages to cover.
User Configuration
As your software grows more complicated you start wanting some way of configuring your software. A way the user can modify its behavior after you delivered it to her. You cannot let them modify the value of C++ variables, because the runnable program knows nothing about the source code anymore.
So you start simple: How about a CSV file with configuration options? But it turns out that is not enough so you switch to INI format. But maybe later you discover that you need nested structures in your configuration file. Let us make an upgrade to JSON!
Before you know it complexity starts growing and you realize you actually need a full blown language. Suddenly you are embedding Lua, Guile, JavaScript or maybe even Python. Lots of games written in C++ use Lua e.g. to define maps or levels.
Oops we are suddenly on the third language now! 3 more languages to cover.
Debugger
The fact that values don’t know what they are at runtime cause problems when debugging. While debugging you want to step through the program and inspect values and see what is going on. To circumvent this problem static language fans added debug information. This of course adds yet more complexity to the build system. Now we need to distinguish between debug builds, release builds and perhaps mixes of the two.
The problem is that while inspecting variables is complex in a statically typed language. There is seldom a standard way to express a user-friendly visualization of it. Often it is just complex data trees of numbers or binary blobs.
To make sense of it all you want to add some kind of viewers. Usually the debuggers come with built in viewers for common types like strings, numbers and array. But you want custom viewers for your own types.
The same problem as before repeats itself. Complexity grows to a point that we yet again need a real language to do the job. A script language which allows us to write some code that explains how e.g. our C++ values should show up in the debugger. Not to mention that debuggers for statically typed languages themselves tend to grow into complex behemoths with a dizzying array of functionality to deal with the complexities of statically typed languages.
Handling this may also need a language. The LLDB debugger can be scripted with Python e.g. Here is an example of creating a viewer for a Swift Rectangle data type by blogger Daniel Martin:
struct MyRect {
let top: Float
let bottom: Float
let left: Float
let right: Float
}
This is the Python code to configure LLDB to show this structure in a nice way in the debugger:
def MyRectSummary(value, internal_dict):
top = value.GetChildMemberWithName("top").GetValueAsSigned()
bottom = value.GetChildMemberWithName("bottom").GetValueAsSigned()
left = value.GetChildMemberWithName("left").GetValueAsSigned()
right = value.GetChildMemberWithName("right").GetValueAsSigned()
width = right - left
height = top - bottom
return "(width = {0}, height = {1})".format(width, height)
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand("type summary add -F " + __name__ + ".MyRectSummary MyModule.MyRect -w swift")
And just like that we are on our fourth case of using a dynamic language while we really just wanted to write code using a statically typed language. Just 2 more languages to go.
Tooling and IDE
Developers like to customize their IDE or programming editor to improve their workflow. In principle we could write customizations in C++, but nobody does that. Microsoft tools would usually use a script language such as VBScript. Emacs use LISP. Sublime use Python. Some use Lua. The point is that the supposedly inferior dynamic languages keep popping up again and again, even if you are a static typing fan and don’t want anything to do with dynamic languages.
Here is an example of extending Emacs with LISP.
(defun simplified-beginning-of-buffer ()
"Move point to the beginning of the buffer; leave mark at previous position."
(interactive)
(push-mark)
(goto-char (point-min)))
Why is this? Well, writing plugins for an IDE in a statically typed language will often be a pain. You would need to be able to compile it and it has to be made in such a way that the binary interfaces are compatible. Not to mention that we are on the right platform. The only possible exceptions are statically typed languages running on a virtual machine such as Java. E.g. the IDEs from JetBrains are written in Java and can be extended with plugins written in Java. However you don’t escape dynamic languages when writing these plugins because you need to use Groovy, a dynamic language to define the build script. Ouch!
So now we are potentially on the fifth language. Just one to go.
Meta programming
As your program grows you will start to increasingly see code duplication and boilerplate code. You want some way of avoiding this repetition. That means you want some kind of meta programming. For a statically typed language performing meta programming using the same language as you program with is next to impossible. Again Zig is an unusual exception to this rule (almost).
Meta languages have to deal with types but if you used a typed language for this you would end up with types having types and quickly complexity would get out of hand.
Hence you see for e.g. the C programming language you have the C-preprocessor as a separate language to handle boilerplate.
For more modern languages like C++, Java and C# one has some sort of templating system or generic programming. C++ template programming is an addition to the language. A language in its own right. It is however a very limited language. I cannot take a generic function written in C++ and use it to process types e.g. They are two separate worlds. This contrasts with dynamic languages where types are just like any other objects. They can be put into dictionaries, arrays searched for, stored or whatever you want.
Ironically as we have repeatedly seen with all these examples, the meta programming language will be more similar to a dynamic language. Except most of the time it is hamstrung by the fact that it happens all inside a compiler and you cannot step through it, debug it or in any way observe what it is doing in detail (this applies to Zig as well).
Hence meta programming is something which tends to get very clunky and difficult in statically typed languages. Some static languages have actually realized this problem and straight out used a proper dynamic language for the task of meta programming. One example is Terra which uses Lua as its meta programming language.
Here is a comparison to understand how this works. Below is a C++ example of defining a template class for an Array.
template<class T>
struct Array {
int N;
T* data;
T get(int i) {
return data[i];
}
};
typedef
Array<float> FloatArray;
And here is the terra version. It is basically Lua code with syntax additions to allow you to write statically typed functions and types.
function Array(T)
struct Array {
N : int
data : &T
}
terra Array:get(i : int)
return self.data[i]
end
return Array
end
FloatArray = Array(float)
Functions starting with terra
are statically typed and data types starting with struct
are statically typed. You can think of terra as a simple statically typed language like C. Instead of the preprocessor they use Lua.
So now we are at our 6th language finally. But we are not actually not yet done with the many problems that plague statically typed languages.
Dynamic Loading
Have you ever heard the expression “DLL hell”? At its core it is a problem caused by static typing.
Because the idea of static typing is that we only care about types at compile time and throw most of the type information away at runtime, we get into major problems when code has to be dynamically loaded.
We have to be really careful about defining binary interfaces and making sure the versions of dynamically loaded library aligns with the version of the code loading the library.
Comparing C/C++ and Objective-C is a nice illustration of this problem. Both languages can load dynamic libraries, however C++ is exclusively statically typed while Objective-C is a hybrid language, where low level structures are statically typed C code while high level object-oriented system is dynamically typed.
Imagine both the main program and the dynamically loaded library (DLL) exchange data using a simple Point
struct.
struct Point {
int x, y;
};
Say the main program passes a Point
object p
to our DLL. The DLL tries to access the y
member by doing p->y
, except this blows up because the application has in fact upgraded and we forgot to update the DLL. Point
is now defined as:
struct Point {
int x, z, y;
};
Names of members such as x
and y
don't exist after compilation. Instead p->y
is translated into address(p) + offset(y)
, which would just be an integer number for the address and an integer for the offset.
However in this case z
is in the offset y
used to be at. Thus we get the wrong value. For this reason binary plugin through DLLs for statically typed languages tended to be quite brittle. It was easy for them to break and crash the whole system.
Contrast this with an Objective-C plugin. Like dynamic languages most communication with objects would not be by integer offsets but through message passing. Instead of p->y
we would write [p y]
which would translate to C code similar to this:
objc_message(p, "y");
This would cause a hash table lookup for the key "y"
. In other words the main application makes no assumption on the memory layout of objects in the plugin or vice versa.
Hence Objective-C plugins tended to be a lot more robust. These problems are one of the reasons why e.g. Microsoft create large complicated standards like COM to create detailed conventions for how programs and plugins could communicate over a binary interface.
With dynamic languages these problems are almost non-existent. Versioning of course matters also in dynamic languages, but software does not as easily break from minor changes. Complex technologies like COM don’t exist in the dynamic world.
What Makes Dynamic Languages Shine
Avoiding all the problems mentioned radically simplifies working with dynamic languages. Rather than relating to a large number of complex tools and systems you focus primarily on your dynamic language of choice. There is no complex build system, IDE, debugger or plugin architecture to learn.
Instead the dynamic language itself is your Swiss army knife. I will use some examples from Julia and Smalltalk to explain how. The Julia REPL is a place where I can run Julia code but which also is a place to do tasks normally associated with a wide variety of separate tools in the statically typed world.
I can add packages to use in my Julia program by writing Julia code. You don’t add DLLs to your C++ program by writing C++ code. The debugger is just a Julia package I load in the Julia REPL and which integrates with it. If I want to display any data type in a different manner I don’t have to learn a special plugin system and plugin language for the debugger. I just write normal Julia code. In contrast I cannot add a C++ method to a C++ class to change how a C++ object is shown in the debugger.
The debugger is also written in standard Julia code, and you always get the source code for packages so modifying the code of the debugger is trivial.
Smalltalk is the same. The IDE is typically written all in Smalltalk and it can be modified at any point to suit your needs even while you are using it. There is no need for a recompilation and relaunch of the program. You can in principle be in the middle of a debug session and decide to change how your IDE works. This allows a very rapid feedback loop where you get to see the effects of your changes immediately.
The selling point of statically typed languages is that you can catch problems before running. But this advantage is to a large part invalidated when the dynamic language user can run the relevant code changed almost immediately after changing it. A static type user in contrast may have to wait considerable time, first on recompilation, then on reloading the program and then by clicking or tapping is way to the particular state which will test the code just changed. That is a slow process. By the time a statically typed code change has been validated by running it and testing it, the dynamic language user could have already gone through multiple iterations of code changes and tests.
Some other examples of functionality built in, is documentation and function and method lookup. A C++ developer cannot write trivial code to lookup what methods his C++ object has and its documentation. Instead a large complex tool has to be built which parses the C++ source code. This tool has to be able to understand all the intricacies of source code dependencies. It is a task for highly skilled specialists to do.
Meanwhile in most dynamic languages documentation strings gets embedded with objects as they are created. When you define a function, that is an expression or statement evaluated which creates a function object. With that function object documentation is stored.
Straightforward Julia code could then be written to lookup and display documentation for a type of function. When loading a module all the types and functions are placed in memory and you can have code which looks up functions and types.
That is why listing methods and doing refactoring is trivial in Smalltalk. As you add methods and types in your IDE they are evaluated. Thus memory already contains a Smalltalk friendly representation of classes and methods.
You can write regular Smalltalk code to inspect these classes and methods yourself. Contrast this with an IDE for a statically typed language. The IDE has to parse all the source code itself. It is very likely to get it wrong, because the build system may contain complexities not easily replicated by the tool responsible for doing refactoring.
A Smalltalk IDE just reuses the representation of types and methods that Smalltalk already uses for evaluated code. A C++ IDE in contrast has to create its own custom representation of C++ code for the purpose of refactoring as C++ creates no usable representation of its objects. Remember types don’t exist for a statically typed language at runtime. Since types don’t exist at runtime you cannot query them and learn things about them which could help you e.g. lookup documentation or refactor.
Thus for a dynamically typed language practitioner his or her language is not merely the language he or she writes software in but also the language in which they create their development tools in. The dynamic language thus because an extremely versatile tool. The static language practitioner is in contrast hamstrung. He or see is forced to learn a multitude of other complex tools to do their job: editors, debuggers, IDEs, build systems, dynamic linking, plugin architectures, documentation tools etc. These tools tend to be large and complex because processing a statically typed language is a difficult task. Unless you are well versed in compiler theory you cannot easily built such tools yourself or tailor them to your needs.
If dynamically typed Languages are superior why do statically typed languages still exist?
Statically typed languages have their place. The point of this article was not to suggest dynamically typed languages are superior at everything. Statically typed languages are clearly superior at systems programming. When you need tight control over performance, memory usage and the hardware they excel. I would not want to write an operating system kernel in a dynamic language.
For realtime systems and many critical system you would want a native language with manual memory management and static type checking. If timing is important you cannot have a Just in time compiler suddenly kick in at a time critical point.
This also applies to games which are sort of like real time systems. That is why you see Android e.g. has struggled to match iOS in fluidity of animations. While Java is statically typed it still runs on a virtual machine, has automatic memory management and JIT compilation. That means you get poor realtime response as in a critical animation sequence the garbage collector or JIT could suddenly kick in causing a delay and lost frames.
Likewise in say a guided missile you cannot suddenly run out of memory. Programs have to have full control over memory usage and keep within resource constraints. For such tasks it makes a lot of sense to use languages like Ada or Rust.
So there is a place for both dynamic and statically typed languages. This article was mainly a response to many articles I see written by static type fans you seem to believe static typing is superior in every aspect and that dynamic languages is really just a form of lazy indulgence.