My theory is that these career evangelists have spent too much of their careers doing just that: evangelizing. They haven't actually been writing code or building software. They've just been telling everyone how they think they should be building software.
I have arrived at the same conclusions as you wrt. static vs dynamic typing. A sufficiently powerful static type system entirely subsumes dynamic typing, and there is quite literally nothing you can accomplish with dynamic typing that you cannot accomplish with such a static type system. After writing software using both dynamically and statically typed languages, this is the conclusion I have arrived at, and it feels like a no-brainer to me.
This is so much of a no-brainer to me that I can't help but be biased when someone disagrees. I want to think that I am open to having my mind changed, but I have yet to see any evidence that sways me.
I do think perhaps a lack of external demand for code quality and correctness might sometimes be at fault. When all you need to do is write the code for the happy path correctly, maybe you just feel static types get in the way. I've converted several javascript projects to typescript, and in every single one of those projects, the conversion exposed multiple(in the tens or sometimes hundreds) cases of incorrect handling of null/undefined or just type mismatches.
I know this has to do with culture and the specific company and engineering discipline, but what I'm trying to get at is that I think a lot of people out there are writing code this way: just hack away until it works, and fix it if it ever crashes. When this is all you want, maybe proving correctness to the compiler is too bothersome. But I don't and won't work this way.
>"My favorite is always the billion dollar mistake of having null in the language. And since JavaScript has both null and undefined, it's the two billion dollar mistake." -Anders Hejlsberg
>"It is by far the most problematic part of language design. And it's a single value that -- ha ha ha ha -- that if only that wasn't there, imagine all the problems we wouldn't have, right? If type systems were designed that way. And some type systems are, and some type systems are getting there, but boy, trying to retrofit that on top of a type system that has null in the first place is quite an undertaking." -Anders Hejlsberg
I like what Anders Hejlsberg says here, but still wonder why he is not pushing for F# adoption/funding/tooling etc. I'm in no position to judge such an accomplished language designer in any way, just curious. Is it ego? NIH? Or that other thing (Whose name someone else can surely provide) about not being able to see the solution if your salary depends on the problem.
Is he in any way related to F#? I feel like he is completely focused on Typescript with C# slowing down with features and F# and VBasic are pretty much in maintenance mode
> just hack away until it works, and fix it if it ever crashes.
This also makes collaboration really hard. Types are a form of documentation. But unlike comments, types are enforced by the compiler. Invariably, lots of comments in a codebase are wrong. Many of those probably started out correct but the code changed and the comment didn't.
Typing is never the limiting factor in my work. Like, if I had to type even double to accomplish the same program, I don't think that would actually slow me down. Given that, the major time difference between using static vs dynamic typing is the investment needed to go from 'throwing things at a wall and seeing what sticks' to understanding what you need to do and doing it.
I've found that "throwing things at the wall" can be very helpful sometimes. It isn't necessarily a linear path from designing a solution -> implementing it in code. Sometimes writing some preliminary code can help flesh out the design. It's a symbiotic process. I've wasted countless hours before trying to design a solution on paper when writing the code out and iterating on the code proved much faster.
And as for how this relates to static v dynamic typing, when I'm roughing out an implementation I much prefer to leave out types.
I find I can hack on things faster with static typing. I'm inevitably picking up new libraries, and using a new library is much easier when it's inputs and outputs are clear. I can see how dynamic would help if you're writing most things yourself, but static definitely has a huge return when interfacing with anything someone else wrote.
totally agree. That's why I think libraries and other mature code should use static typing, whereas I like to use dynamic typing for code that I am prototyping. This is why I love Typescript, you can have both :)
Anybody who thinks that avoiding extra typing is the point of dynamic langauges, or that working in a dynamic language is simply throwing things at a wall to see what sticks is completely missing the point.
The whole point of dynamic languages is that your problem space is in fact dynamic. In many domains your data simply defies any ontology you try and force it into. Static typing systems themselves are somewhat acknowleging this problem internally with the differences between nominal and structural based typing system.
Dynamic langauges are about changing your view on data where instead of shoving all your data into and out of boxes that don't fit, you just see entities as arbitrary collections of attributes and thus typing them makes little sense.
The gist is basically that a dynamic type system doesn't afford you any more power or "dynamicism" than a statically typed language.
At any point in time, when writing code, you always know something about your data. Even if it was quite literally totally unstructured JSON data with no rhyme or reason to its structure other than it being any valid JSON, then at least you know that much, and you can reify this in the type system: `data JSONVal = Map String JSONVal | [JSONVal] | JSONString String | ..`
It is impossible to write code that works on "any kind of data" because your code, and what it does with the data, makes the same kinds of assumptions about the shape of the data as static typing does. The only difference is that a static type system will let the compiler help you find obvious bugs at compile-time, while dynamic typing will force you write tests up the wazoo and the end result will still be something that is less robust wrt. refactoring than a statically typed language. The statically typed language just gives you this all for free.
Great blog post, great blog in general, but here's the highlight of all that for me:
"In contrast, most static type systems do not allow such free-form manipulation of records because records are not maps at all but unique types distinct from all other types. These types are uniquely identified by their (fully-qualified) name, hence the term nominal typing. If you wish to take a subselection of a struct’s fields, you must define an entirely new struct; doing this often creates an explosion of awkward boilerplate."
That is exactly the space I want to work in. I want to treat entities as anonymous entities without having to name them them since at that point I approach a 1:1 mapping of names to things in my system. I want to use generic data operations to merge and slice my entities which are just collections of facts about a thing. Structural typing is a step towards this programming model but still requires that you shove your data into some ontology.
EDIT: Actually I take that back, that blog post is totally missing the point. She asserts that the type system just makes explicit the implicit requirements of your code. The problem is in HOW the type system does that. By focusing on the function itself, the author ignores the real problem further upstream. If there's some type, then I have to shove my dynamic data into your type in order to use the function. And if you're trying to use interfaces, there's still some concrete type that you've created to attach that interface too. Either way, you haven't managed to support dynamic data through your system.
You do refer to specific attributes in the code, doesn’t you? So all in all you at least use a structural type with some given attributes. Even if you only ever use those attributes, you are already better off with regards to future changes, refactors, etc.
> shoving all your data into and out of boxes that don't fit
Ironically this is what dynamic languages do. Everything is in boxes that could be anything.
It's worth bearing in mind that dynamic languages can be (and often are) built using statically typed languages. You can easily build dynamic collections using static typing.
> I have arrived at the same conclusions as you wrt. static vs dynamic typing. A sufficiently powerful static type system entirely subsumes dynamic typing, and there is quite literally nothing you can accomplish with dynamic typing that you cannot accomplish with such a static type system. After writing software using both dynamically and statically typed languages, this is the conclusion I have arrived at, and it feels like a no-brainer to me.
I've come to the same conclusion. A dynamic language is a footgun that doesn't buy any additional power or expressiveness over a well-developed typed language.
It gets even worse when dynamic language developers start interacting with data. The "just do whatever" mentality that rejects type design also rejects relational design without any consideration of whether it's (almost certainly) a better choice than a document database.
I find the comment that dynamic typing rejects relational design funny when dynamic typing is what makes working with relational data relationally tractable. Static typing falls down almost immediately when trying to work with your relational data and ends up requiring reams of glue or mapping frameworks just to make your relational data approachable.
It's a feature, not a bug, that the code blows up when the database schema doesn't match my models.
Just as the necessity of DI frameworks isn't an argument against unit testing or the inversion principle, the necessity of ORM frameworks isn't an argument against static typing or relational databases.
It's not the schema matching your model, it's the result of any query.
How many different types do you need to represent the result of all possible projections on a single 10 attribute relation? Now join that to another 10 attribute relation, how many types to represent all possible projections of a single join between two relations? And Maybe is not the answer, Maybe is a lie. The data is either part of the result set or it is not based on the query.
You only need to map the database types to the language types at worst. So int, string, timestamps, binary, maybe GUID. Or you could just use string for all data and then you're pretty much matching a dynamic language.
Results from joins between arbitrary queries are just lists of these types per field.
The problem isn't the attributes. I WISH there was a dynamic language that would statically type your attributes, that would be the perfect hybrid for me.
The problem is typing the agregates. So for all projections from a single table, that's 2^9 types. For all projections for a join on two 10 attribute relations, 2^19. This is in the realm of yes you can do this in a static type system, but why?
Most ORMs don't let us do this, and it works well.
Getting back the entire relation (rather than some sub-tuple) isn't the kind of expense that causes problems scaling. It chews up network bandwidth between the database and the API, but it's not more computationally expensive (asymptotically).
And if my entities are dozens of columns wide, I'm probably not in 3NF anyway, so it'd be better to focus my efforts on getting there.
Why do you want to have a static type for each combination of fields in a query? There's no need to do this.
> I WISH there was a dynamic language that would statically type your attributes, that would be the perfect hybrid for me.
In statically typed database frameworks it's often the other way round: you have pseudo-dynamic typing for fields but they're static under the hood, sub-typed for the specific database type and/or stored as raw bytes.
This is basically "boxing" fields - exactly the same principle as dynamically typed languages do behind the scenes, except the library will have customised "boxing" designed for the subset of types the database supports, e.g., typed per field column rather than for every individual field in the result (like dynamic languages). This hugely reduces the overhead of actual boxing as you don't need to store the type of the field or indirect to its data.
This is sort of like dynamic typing just for the database, except the possible types are focused on a specific subset the database supports and therefore can be made efficient for the task.
Dynamically typed languages must cater for reassigning properties, fields, and/or types ad hoc, and must be made much more general - AKA slow.
For example you could (in pseudocode) do:
procedure showTitles(queryResults: QueryResults, titleName: string):
for title in queryResults.fieldData(titleName):
let str = title.getString # Returns a string type.
display str
let queryResults = db.query("SELECT * FROM MYTABLE")
showTitles(queryResults, "title")
You get the benefits of static typing and dynamic typing; type errors are caught at compile time, and you explicitly or implicitly convert the "dynamic" box for the field. For instance, trying to pass an object that isn't a `QueryResult` to the `showTitles` procedure will throw a compile time error. In dynamic languages, you could pass anything to that procedure and have to hope it doesn't blow up at run time. Testing all the possible paths and dynamic types that could be given to this procedure could be a combinational nightmare, so "to be safe" you'll have to check the type is the equivilent to a `QueryResult` anyway at run time...
The `getString` function can do whatever it needs to do to convert the data if it's not stored as a string (or throw an error if this isn't possible/appropriate), whilst maintaining type relationships once it's out of the pseudo-boxed type and ensuring memory used is appropriate to the "real" type.
Source: I've written database query frameworks for statically typed languages.
Not my experience at all. I am much more productive when using types to interact with data. And I have programmed in Lisp, JavaScript etc. so I have real world experience comparing the two world views. I wonder if you have actually worked with typed languages and relational data in an effective way?
> A sufficiently powerful static type system entirely subsumes dynamic typing, and there is quite literally nothing you can accomplish with dynamic typing that you cannot accomplish with such a static type system. After writing software using both dynamically and statically typed languages, this is the conclusion I have arrived at, and it feels like a no-brainer to me.
Do you thing some existing practical, high-usability programming language has a sufficiently powerful type system in the way you describe?
For me the big thing is that static type systems tend to be simultaneously too powerless to express what I need, and too laborious in demanding manual specification of things that I don't actually need to specify for the program to work. In a dynamic language with a good schema system like malli or spec in Clojure, I can have easily customizable verification that is opt-in.
I prefer dynamic typing when writing code that I perceive as "volatile". This is especially true when it's a piece of code that's currently only being worked on by me. Static typing adds boilerplate that feels cumbersome, especially when the types of the variables are already made obvious by the variable names.
However when a piece of code becomes mature, doesn't change often, and starts to be depended on by other people (eg a core library), then I start adding types for additional safety and documentation. This system of gradually adding types when needed is why Typescript is so great for me.
That’s not necessarily true anymore. Languages like Haskell have both strong static typing and very good type inference.
> However when a piece of code becomes mature, doesn't change often, and starts to be depended on by other people (eg a core library), then I start adding types for additional safety and documentation.
That’s a practice in Haskell development as well, but the safety and guarantee of consistency are already present prior to the annotations.
I can see that, but find myself with different preferences. When I know I'll be making changes, I want to be working in a setting where I can tell some checker what I'm assuming, so that it lets me know when work elsewhere invalidates that assumption. Assumptions that will be mechanically checked are assumptions I can (metaphorically) page out in my head as I focus on other things, to be alerted when they again become relevant.
I definitely agree that all of this becomes even more important when those assumptions need to be shared between the heads of different individuals.
Tests are a very underpowered replacement for types. They require a lot more work to create, much more maintenance, and bring you much less confidence. Unity tests are the worst kind on each of those dimensions.
It's really not a good idea to follow TDD instead of using types. You want tests for things that aren't practical to verify, not for replacing static verification.
If you're using Any, then your program is (at least in part) dynamically typed. I don't think it's fair to say "look! Static typing is better than dynamic typing in any and all situations! (so long as it can use dynamic typing)"
I have arrived at the same conclusions as you wrt. static vs dynamic typing. A sufficiently powerful static type system entirely subsumes dynamic typing, and there is quite literally nothing you can accomplish with dynamic typing that you cannot accomplish with such a static type system. After writing software using both dynamically and statically typed languages, this is the conclusion I have arrived at, and it feels like a no-brainer to me.
This is so much of a no-brainer to me that I can't help but be biased when someone disagrees. I want to think that I am open to having my mind changed, but I have yet to see any evidence that sways me.
I do think perhaps a lack of external demand for code quality and correctness might sometimes be at fault. When all you need to do is write the code for the happy path correctly, maybe you just feel static types get in the way. I've converted several javascript projects to typescript, and in every single one of those projects, the conversion exposed multiple(in the tens or sometimes hundreds) cases of incorrect handling of null/undefined or just type mismatches.
I know this has to do with culture and the specific company and engineering discipline, but what I'm trying to get at is that I think a lot of people out there are writing code this way: just hack away until it works, and fix it if it ever crashes. When this is all you want, maybe proving correctness to the compiler is too bothersome. But I don't and won't work this way.