I’ve been keeping in mind an analysis of what is possible in the super-dynamic languages of Ruby and JavaScript while working on features and such, trying to find patterns that just wouldn’t be possible in Rust (which I’m taking as a standard for a great type system). I can’t think of much that I’ve seen done via metaprogramming in a Rails app, for example, that wouldn’t be just as well served (nay, better) by compile-time macros or even regular modules and function calls.
To be clear, it is quite possible to program in an extremely dynamic style, even in a statically typed language like Rust!
For example, you can create a Vec
where each element is either a string or float, and you can then do dynamic type checks to determine whether an element is a string or float:
enum StringOrFloat {
DString(String),
DFloat(f64),
}
let x = vec![
StringOrFloat::DString("Hi!".to_string()),
StringOrFloat::DFloat(5.0),
];
match &x[0] {
StringOrFloat::DString(value) => println!("It's a string! {}", value),
StringOrFloat::DFloat(value) => println!("It's a float! {}", value),
}
What we've done here is created a new static type called StringOrFloat
. At runtime the StringOrFloat
type is either DString
or DFloat
(which are short for "dynamic string" and "dynamic float").
We can then put both DString
and DFloat
inside of the Vec
(even though Vec
has the restriction that all of its elements must be of the same type!)
And when we extract elements out of the Vec
(such as by using x[0]
), we can do dynamic runtime checks (using match
) to determine whether it's a DString
or DFloat
.
There's no trickery or complicated metaprogramming here: just simple ADTs/enums.
To give a comparison, the above Rust program is equivalent to this JavaScript program:
let x = [
"Hi!",
5.0
];
let value = x[0];
if (typeof value === "string") {
console.log("It's a string!", value);
} else {
console.log("It's a float!", value);
}
As you can see, the Rust program does indeed require more keyboard typing (you have to declare a new StringOrFloat
enum, and you need to use StringOrFloat::DString
and StringOrFloat::DFloat
).
However, fundamentally it is doing the same thing as JavaScript: when you create a StringOrFloat::DString
or StringOrFloat::DFloat
, it creates a runtime tag which "remembers" whether it's a DString
or DFloat
. And then match
uses that tag to do runtime checks. This is exactly the same as the type tag which is used by dynamically-typed languages.
So, in principle, anything that dynamically typed languages can do, Rust can do, because Rust has ADTs/enums: Rust functions can accept dynamically typed arguments, and they can also return dynamically typed values. And Rust structs/enums can have fields which are dynamically typed. It just requires some extra work (to declare and use an enum).
Whether it's convenient or idiomatic is a different question, but at least it's possible (without needing to write an interpreter).
There's also even more advanced stuff, such as the Any
trait, which can be used to do some pretty crazy things (notice that the is_string
function in the example can be called with any 'static
Rust type, and it will do a dynamic type check to determine whether that argument is a String
or not!)
With that out of the way, let me answer the OP's question. I've been programming in JavaScript for over 12 years (and I've used many other dynamically typed languages).
I eventually fell in love with statically typed languages (well, good statically typed languages...) because of their ability to confidently refactor code. In my opinion this is by far the most important benefit of static typing: doing significant refactoring in a large code-base in a dynamically typed language is possible, but it takes forever and often creates new bugs. That is not the case with (good) statically typed languages.
However, there are downsides of static types:
-
You are required to name your static types, and naming things is hard. So increasing the amount of things you need to name is really not great.
-
Static typing strongly pushes you into certain designs, which is usually a good thing, but it sometimes requires a lot of effort to make certain programs fit within those designs.
For example, a lot of programs are easier to write with some sort of duck-typing. But static typing doesn't have that, so you have to completely change the design to fit within static typing.
Another example is certain kinds of programs which are fundamentally dynamic, such as retrieving some JSON from a web server and then parsing it.
I had to write a Rust program which opens a CSV file and then extracts some data from it. I used some CSV-parsing crates to do the bulk of the work, but it still required some ugly code on my part:
for result in reader.deserialize() {
let (character1, character2, winner, _strategy, _prediction, tier, mode, odds, duration, _crowd_favorite, _illuminati_favorite, _date):
(String, String, String, String, String, String, String, String, u32, String, String, String) = result?;
...
}
I imagine parsing JSON (with some complex data) will be much worse.
-
Static typing often requires a lot more type casting/type conversion functions. Dynamically typed languages let you delay the conversion until the final moment, whereas statically typed languages often require you to do the conversion immediately. In some cases dynamically typed languages don't need to do type conversion at all!
-
Static typing is more annoying when you are quickly slapping together a prototype, or you're half-way through a big refactoring, and the compiler forces you to fix all of the errors, even if you know that the program is fine at runtime (lemme just test my program, dammit!)
-
Static typing forces you to design your program up-front. This is usually a good thing! But it does mean that the initial prototyping stage takes longer, because you can't just slap things together.
I find that I spend a lot more time thinking about things with static typing. Usually this thinking is about "how can I make the compiler happy?". But I don't mean that in a bad way: the compiler is usually unhappy because my code is wrong! So making the compiler happy is the same as writing correct code.
Nonetheless, the fact that you are forced to do things correctly does take more time up-front (with big pay-offs in the long-term).
On the other hand, static typing makes it easier to refactor (even during the prototyping stage!), and the fact that it forces you to design things properly means that it's actually reasonable to push your prototype into production (which inevitably happens with every language).
-
This isn't actually a problem with static typing per se, but in my experience statically typed languages take a long time to compile. This does slow down the development experience a lot.
-
This also isn't actually a problem with static typing per se, but in my experience certain languages (I'm looking at you Haskell and PureScript) have a tendency to go really crazy with the type-level stuff.
This isn't always a bad thing: static proofs are great! But the complexity can be very overwhelming, especially if you aren't used to it yet.
Thankfully Rust has managed to (mostly) avoid this problem, so its type system remains accessible even to non-functional programmers.
To be clear, I love Haskell and PureScript, but I feel like they sometimes go a bit too far in their pursuit of purity and correctness at the expense of practicality.
-
I'm not sure how true it is, but to me it "feels" like it's easier for a beginner to learn a dynamically typed language rather than a statically typed language. This doesn't matter when you're experienced, but it is a downside of statically typed languages for beginners.
Those are the ones I can think of off the top of my head.
After I embraced static typing, I actually haven't had too many problems with it. It does help a lot that the statically typed languages I use (including Rust) have typeclasses and ADTs/enums. Those two features dramatically increase the flexibility of static typing, making it quite enjoyable overall.
Most of my problems with Rust are due to its strict memory model (including references), not really with the static type system itself.
If we're talking about problems with a specific statically-typed language (as opposed to static typing in general), I could talk for hours about the problems I've had with TypeScript...