The only thing that really frustrates me in this discussion is that it's all about how people "feel" and without empiric evidence.
The research out there found no meaningful difference between both styles (unless there's newer research I haven't seen?) and people keep taunting around how their preferred side is undoubtedly the right one.
That's just like, your opinion man.
Personally I like typed languages but the type systems in languages like TypeScript are simply insufficient. You still end up with runtime bugs because you can't actually use those types at runtime. You can't encode a lot of the runtime logic into the type system so you're still manually checking impossibilities everywhere. I find myself even having to create local variables just to make typescript detect an obvious condition.
If a type system could basically remove the need for me to think about runtime bugs, then that's an absolute killer feature that I doubt anyone would argue against. But most languages don't provide that, so you're stuck in this halfway point where you have all this overhead, some benefits, but you still can't 100% trust your types.
As for why there are no meaningful differences in bugs, speed, etc my guess is that it all evens out. Without the type system safety net you are much more likely to test your code and as a result less bugs go in. On the other side people rely too much on the type system that's not good enough and then still end up with the same amount of runtime bugs. On one side you write code faster, but you have to test more, so it also evens out with writing more boilerplate, but with less tests.
I really wanted some hard research on this, but I know it's a hard one.
I think most people would agree that types prevent a lot of bugs (and others posted research to support it in the thread), so the question is less about that and more about the subjective reasoning people have to decide the investment is not worth it.
>I think most people would agree that types prevent a lot of bugs
Again, that's not what research shows. Type checking bugs are most definitely not the most common ones and typing doesn't really help with runtime bugs unless it's a strongly typed language.
I'm all for types, but after using TypeScript and seeing everyone (including the article) touting it as the solution, it simply isn't all that better than vanilla JS.
I did spend countless hours having to wrangle typescript when integrating with third party libraries, searching for types that were not always there, manually extending/creating library types and so on.
So no, it's not as clear cut as some say it is.
If we want to argue for types, then we should use strongly typed languages that prevent these bugs (if you can manage to create the right types, of course). Examples in TypeScript are simply not going to cut it because that's a horrible example of a typed language. I mean, it's not even a language.
Edit: above all, hard research. If there's research for this, why isn't that in the article?
> then we should use strongly typed languages that prevent these bugs
So Typescript then? The most common recurring bugs on my team is front-end devs not knowing the shape or property names of types written by other devs (backend, other font-end, themselves a month ago). If we moved to Typescript they would get those little spellcheck-like underlines pointing out these errors before they attempt to commit, rather than customer's pointing it out.
Sounds like an argument against untrusted input, I just want Typescript so at least my own team becomes trusted input.
> Type checking bugs are most definitely not the most common ones
This is not a facetious question, so bear with me: what is a "type checking bug"?
Do you mean "a bug in language L that would have been caught by the typechecker T, if only L had it"?
That's the only meaning I can think of that makes sense [1] (ok, thought of another one [2]). But what does it mean that "most bugs" aren't of that kind? Is it that "most bugs are of a kind that cannot be caught by any conceivable typechecker"?
--
[1] We can safely disregard the other possible interpretation of "a type checking bug is a bug in the typechecker itself" ;)
[2] Maybe "this expected a list but I passed it an integer instead"? I do occasionally trip over those with Python, but I think most typecheckers are more advanced than that, and can check for more interesting classes of bugs.
Forgive me if I didn't use the right terminology too. What I mean by type checking bugs are the kinds of bugs shown in the article: I have an argument that should be a number but someone accidentally uses it with a string instead.
That's the most basic bug that typed languages catch. But for me, those are extremely simplistic. I don't need a type checker to know that. If you follow the code path you already know the types you're dealing with.
What strongly typed languages can provide are types with constraints. E.g. an age is not only a number, but a valid number greater than zero (maybe with an upper bound?) that is attached to a person. That kind of strong guarantee will definitely catch runtime bugs if you pass in some other numeric value that is not a representation of an age.
Languages like TypeScript can't do that. Take the example below. It compiles as if there's nothing wrong:
type Age = number
type Person = {
age: Age
}
const person = { age: 10 } as Person
const someRandomNumber = 10
function printAge(age: Age) {
return console.log(age)
}
printAge(person.age)
printAge(someRandomNumber)
This is the kind of bug that a good type system would be extremely helpful with. In my entire career it's definitely a lot more common than passing a string by mistake. This can be extended for having a type that differentiates an `Email` from a `ValidEmail`, an `ActiveUser` from an `User` and so on. Those are, in my view, where a type system can truly help us catch bugs. But even if you have a type system that supports that, nothing stops someone from simply using `number` instead of `Age`.
In any case, that's why I don't think it's as clear cut as people put it. Yes, in very rare occasions TS will tell me I accidentally allowed something that can be undefined pass (excluding the many times it gets it wrong). However that doesn't come for free and that is what makes absolutist claims for either side unhelpful.
TypeScript is intentionally _structurally_ typed (except for enums). The example you describe would only be caught by a _nominally_ typed language (think Java).
Structural typing is great when programming functionally (i.e. with immutability) when the most important thing is the shape of inputs and outputs of functions, instead of named objects (like Person) and properties. In a functional program, for example, "printAge" would likely be called something like "print" or "printNumber" since that is what it is doing to its input _value_.
I think a lot of the misunderstanding I've seen recently around TypeScript (like from the Rails creator) comes from the misuse of TypeScript - if you use TypeScript in an object-oriented way, its going to be significantly less helpful.
I don't follow this one. I've never seen anyone use TypeScript with an OO approach aside from, ironically, the .NET folks.
The code I wrote has nothing OO with it and we can already see the issues. The majority of TS I've ever worked with was written for React and it still would benefit greatly from nominal types as you call them (thanks I didn't know that terminology).
I don't see everyone misusing TS. For me, it's simply a very limited language as far as typed languages go. As a result, it's a shame that it's what is being touted as a good example of why you should use typed languages.
That hasn’t been my experience. ReScript and Elm are much better compared to the fragile types I’ve encountered with Typescript (where I needed to write code to do the type checking). Happy if you’ve found something that works for you though.
For such problems we use the Opaque type from the nifty type-fest package. We use it for all of our id types (which are usually numbers) so we can't accidentally mix them.
In your example it would be:
type Age = Opaque<number, "Age">
And then you wouldn't be able to pass a random number to printAge
> Again, that's not what research shows. Type checking bugs are most definitely not the most common ones and typing doesn't really help with runtime bugs unless it's a strongly typed language.
> typing doesn't really help with runtime bugs unless it's a strongly typed language
I’m skeptical of this research. Typescript prevents runtime bugs for me day in day out. It also allows me to produce much more elegant and flexible solutions which would be infeasible in plain JavaScript. When people say there is no significant benefit compared to JavaScript it almost feels like I must be on a different planet or something. I would love to see what kind of code these people are working with, or how they’re attempting to leverage the type system.
Would you still favor Typescript if it was an actual static language: such that a type problem not caught by Typescript potentially corrupted memory and crashed the browser or Node.js instance?
> such that a type problem not caught by Typescript potentially corrupted memory
What does this have to do with static types? High-level languages, typed or untyped, are generally memory-safe. They'd need a very good excuse not to be. If Haskell isn't a "real" statically typed language, nothing is.
As per your link is, it is not so much that "The research out there found no meaningful difference between both styles", but more than the research out there is utter garbage.
Which is what I would expect: a proper research on the topic would be terribly expensive, requiring multiple teams reimplementing complex projects in different languages and separate distinct expert panels evaluating development and results.
Yeah this is the unfortunate reality of software engineering methodology research. As a field we simply don't have strong and reliable research demonstrating what makes teams more productive or software more reliable. And we don't even seem to have new methodological ideas on the horizon to come save us.
Yeah, which is a shame. Because I truly wanted to be confident that the costs of these don't outweigh the benefits.
Regardless if it's no meaningful difference or no good research, the point remains: we simply don't know, so these discussions are about opinions for either side, not facts.
No, the point does not remain that “research” “shows” anything. Seems there is agreement that ‘meaningful experiments have never been done because it is too complex and expensive’ so there is no significant research data.
But the basic argument is that a programming approach — having a compiler and a type system is not a “style” btw — that employs development time tools to reduce the burden of runtime operational tools, afford greater application of a wider set of optimization techniques at runtime, and also add to the information bandwidth of source code via type annotations is reasonably expected to be more rigorous than the other approach.
Fair point. If there was good research showing no difference, it would be equally pointless to argue either side is right, but it wouldn't be mere opinions, like they are today.
As for the move between runtime to development time tools, it still misses the cost of it.
We could move all our code to Haskell and have absolute guarantees for a lot of the common found bugs, but we don't because it's costly. And I don't mean rewrite cost, I mean the cost of its own complexities.
Nobody argues that typed languages aren't more rigorous, but that's not the only variable we care about.
Rigor is not the only variable, agreed. The issue (again) is that the other variables are many and they are non-linear in the main. (For example, the variable of ‘competence of development organization’ is not smoothly distributed. Our field is not a “smooth” ___domain it is ‘chunky’.)
So where does that leave us? Opinions are one option - comparative views to other ‘industrial’ age type of activities may be informative.
I propose to you that “we moderns” live in a typed world. It is not strongly typed but it is typed. One could argue that that is a side-effect of physical world artifacts produced at scale. I would be interested in hearing the argument as to why that near universal phenomena* does not apply to software, in your opinion.
(* Industrial production at scale and emergence of standards)
Maybe my point got lost within the threads but I never said types aren't useful or that we should reject them at all. After all, what you said is true. Whether we want it or not, everything is "typed" in one way or another.
My issue is a practical one. Using limited typed languages like TS has several drawbacks for little benefit. Using a strongly typed language like Haskell would add a ton of greatly needed rigor and correctness, but it's also not without huge drawbacks. Same goes for dynamic languages.
It's not a question of whether or not we should model our software based on our worldly types, it's about how strict we should be about it and the benefits and drawbacks that come within this spectrum. For that reason I argue there's no single general answer to this and claiming there is one is nonsense.
> Again, that's not what research shows. Type checking bugs are most definitely not the most common ones
It's likely not the most common bug family to reach production or to be published on github so it will be visible to researchers doing their studies (survivorship bias…), but they are definitely the most common bug I'm facing when using an untyped language, and the majority of developers shares this sentiment.
Of course you'd argue that personal feeling isn't science, but studies falling to survivorship bias aren't good science either…
Not all of that research was done with public code. But they had other flaws.
I should have prefaced that with "IMO". That particular sentence comes from my experience/career, so YMMV.
When I'm reading code, I know what types I'm dealing with. Even more so if I'm creating that code. But then again, I worked with dynamic languages for a long time and this could be a skill I picked up because of it.
That's the first thing you do: read/learn the data structures you're working with. As Linus said once:
> Bad programmers worry about the code. Good programmers worry about data structures and their relationships.
Of course I still have to read and learn that. But that's the same with or without a typed language. Whether your data is an unstructured hash or a carefully typed structure, you need to know it.
Way too many people rely on ctrl+space based programming, figuring out everything as they go. One aspect that dynamic typed languages forces into us (at least it does to me), is to learn the data structure and relationships more thoroughly.
That's great unless you are working with a codebase that spans decades and tens of millions of lines of code. At that point if for every assignment you are tracing through your data types manually - you are out of a job.
Also, if I am using an API another team is providing - I don't want to have to go and trace through their code.
And that's assuming you even have access to said source code. In many cases, you just receive a semi-documented JSON object and a prayer.
Anecdotal story:
We recently went through an effort to introduce strict-type checking for our typescript backend. As part of that effort, we introduced concrete types for every API accessible across a service boundary, which caught 6 mismatches between the documentation and the implementation (e.g. return Promise<bool> vs Promise<Instance>) and several hundred issues with missing null or type checks that would result in an "InternalServerError" instead of a meaningful error message.
We started rolling the strict version out this week, and we can already see a meaningful improvement in the sentry logs.
> Type checking bugs are most definitely not the most common ones and typing doesn't really help with runtime bugs unless it's a strongly typed language.
TypeScript actually performed quite well in the last academic study I read on this debate (like 6 years ago).
The real problem with web APIs is that there is always some lossy conversion between type systems as we cross boundaries. So we can't really make some sort of closed system assumption. A typical web app may interact with dozens of API services, some run by third parties. And maybe you can trust their API docs, but maybe not really, and they're subject to updates anyways, so you always have to be on your toes.
Even internally, you can't really control all the type info from end to end. Even the most monolithic systems will have some sort of abstraction leak when going from JSON -> Object -> Relational storage. Even largely monolithic systems will typically break off some functionality (like email sending, websockets handling, etc) as a separate service. The boundary creates a co-evolving connection between separate services run by separate teams, with separate upgrade cycles, even without full microservices buy in. And that creates potential runtime type errors when mapping between these layers.
Even if you've somehow plugged all those leaky abstractions and your tight type system has handled all the edge cases and is provably correct: the user will teach you otherwise on the UI layer. User input can vary wildly, and everything from device capabilities to personal disabilities to network throttling to authentication to using weird ISO characters to file sizes to strange input devices and legacy systems with their own quirks, will completely throw you off at some point.
So no matter how type safe your language is, you'll always have to deal with the untyped and unpredictable user layer. And the reason why JS has been so successful there is because of how flexible it is. It's not as painful to make quick tweaks with JS as it is with a type system like Rust's, for example.
JS, for all its quirks, made a good amount of trade-offs for its target platform.
Type errors are almost always the easiest types of errors to fix. What really will get you is debugging interdependent systems and services, and that pesky user layer. But people will spend extraordinary amounts of time maintaining complex type systems just so their OCD can be satisfied about believing, that at least for a moment, if their program compiles...that all is right with the world for that brief moment just before you deploy to production.
Oh were that the case. I do think types are essential for mission and life critical systems, but testing is even more essential for those cases, so everything should already be thoroughly covered. For consumer apps, however, is it worth the cost in velocity?
Nope, static typing triples the average bug count of a piece of software. Using static typing add complexity to your code and complexity leads to more bugs.
Static typing is where you significantly decrease your development speed and significantly increase your bug count in order to increase the performance of your software.
Static typing only increases bug count when devs who can't be bothered writing proper code cast stuff willy nilly/hack the code into working rather than put the effort in to growing and pruning the type system as needed. At least from what I've seen.
But even then that's only as bad as not using any types at all, the statement that statically typed languages create more bugs is just plain ridiculous - there's a reason that space-faring agencies and defence contractors exclusively use statically typed languages with very strong rulesets on how code is written.
Well Java syntax is very verbose, but I'm referring to the effects of static typing on the program structure.
Dynamically typed programs have much simpler structures and are much easier to reason about and test than their static typed counterparts.
The larger the program you write with static typing, the worse it becomes compared to it's dynamically typed counterpart. The internal complexity of statically typed programs grows at a much higher rate. This increases development times and decreases program correctness.
Through I don't think I can really do a fair comparsion with a functional language like Scala. I've only used Scala for 2 weeks on a single project. Functional languages tend to increase code correctness by trading away performance.
Sorry for being a pain, but I’m still not seeing it. You gave an example of LOC but now have stepped back again into speaking in generalities. What structures and what complexities are you talking about? Give me examples. Tbh I have no idea what you’re talking about. ELI5.
Roughly stuff like templating, abstract base classes, generics and non-trivial user defined types.
Consider a simple program that adds 2 numbers: a & b together. In a dynamically typed language, this would be a + b. In a statically typed language, once you consider overflow and underflow, you will likely have a hundred lines of code.
This creeps up again in JSON parsing where the types really are defined at runtime.
But the most general case is people creating complex and hard to understand types in their code, which they then export to unsuspecting developers to use. e.g. The type of stuff that forced C++ to introduce the auto keyword.
> In a dynamically typed language, this would be a + b. In a statically typed language, once you consider overflow and underflow, you will likely have a hundred lines of code.
literally a single line of code to overload an operator.
anyway, if you don't consider error states in your dynamic implementation and you do this in static then yes, it could be simpler. But static doesn't force you to consider error conditions - just like this C++ example does not.
and yes, if you fall back on javascript notions of default cross-type arithmetic (1 + "1" = ?) then you will get something, and if you take great care to structure your code you may get something useful as a side-effect, but, this is not meaningfully different from the error-state for most people. Getting [] or "11" isn't the expected outcome unless you've come to expect that quirk of javascript, and is something that a static compiler would rightfully complain about - because a user asking for an undefined operation computed across incompatible types seems like a classic type error. If it's not, then just define the operators that are meaningful to you - and you could define it as an interface if you wanted it to work with a bunch of classes etc.
Again though, the complaint elsewhere that "a lot of this debate just ends up being dynamic programmers who are unaccustomed to using static types at all and think it must be so super burdensome all the time" seems pretty accurate. Defining custom operators is not something that occupies even 1% of my time in any static language.
The code you posted is incorrect and full of bugs. For example, if A is 9223372036854775807 and B is 10, then you will get the incorrect answer. Try it out if you don't believe me.
Another example is A is -9223372036854775807 and B is -10. Again you will get an incorrect result.
Once you finish fixing these bugs it will be at least 100 lines long. The dynamically typed version of the code is just A + B.
"a lot of this debate just ends up being dynamic programmers who are unaccustomed to using static types at all"
Static typing is objectively the inferior approach. It's just that people are too lazy to learn how to code with two different typing systems.
Overall statically typed programmers only code at 1/3 of the speed as their dynamically typed counterparts. Of course, if you have only ever done statically typed programming, you are not going to understand how bad it is in comparsion.
It's really hard to understand what you mean. Could you clarify, taking into account these two results, firstly a correct calculation from a statically typed language, Haskell, and secondly an incorrect calculation from a dynamically typed language, Python
I think we are at max reply depth but here is the trivial dynamically typed solution to the problem:
https://pastebin.com/UZvB5YD1
It's a little bit simpler than anything you will get in an statically typed language. It works for integer and floats of any size. It even works for lists!
You can't do that in a statically typed language without a ton of code.
That doesn’t sound like it’s typings fault at all. That’s a language design choice. Typed languages have types that handle adding numbers of all sizes same as dynamic typing.
In fact JavaScript numbers aren’t magic, they are 64bit doubles, which have their own strange behavior and special cases, many of which could be considered “incorrect”. You’re acting like dynamic typing solves problems that it doesn’t solve.
Is that really a fair comparison? The dynamically typed language is also typed, probably a longint. Had the C++ example used long then these examples would be at least bug compatible.
If we're discussing weakly typed languages then this argument is more valid, but that opens up to a new class of bugs strong dynamically typed languages don't have.
Anyone who says the conclusion is obvious have probably not thought this through.
That's not something inherent to dynamic typing. If we take a specific language such as Python, that specific behaviour has changed between versions.
Much too often, any discussion of typing systems often boils down to specific traits around someone's favourite language. These specifics are obviously important enough to warrant a lot of skepticism around too general conclusions from studies.
Bascially we're all recounting anecdotes, so let's be honest about that. I don't doubt yours, I just don't think they necessarily reflect fundamental truths about programming.
You're explicitly comparing apples and oranges here. Are you implying that it's never necessary to check for overflow and underflow in dynamically typed languages, and that it's somehow mandatory to do so in statically typed ones? Or, in case your argument is "numbers in dynamically typed languages do not overflow", are you aware of BigDecimal in Java or sys.maxint in Python 2?
In modern dynamically typed languages, you do not need to check for overflow or underflow and you don't need any special library to do so, it's built directly into the language.
Yes, running arbitrary code at compile-time can produce arbitrarily complicated results if you're not very very careful. This is not a type system issue: lisp macros are just as dangerous.
> abstract base classes
Again, nothing to do with types: your complaint is with Java-style OO. Which is garbage, but for historical Java-specific reasons. Using Java as your prototypical example of a typed language is like using PHP as your prototypical example of a dynamic language. There's no such thing as a feature good enough to rescue a bad language. That doesn't mean no feature of a bad language can ever be good.
> generics
Generics produce strictly simpler code than copy-pasting the same implementation over and over again. That's their entire purpose. A generic function isn't a way to give the same name to different pieces of code (the way that inheritance is, for instance), it's literally one function: `mapIntList` is `mapFloatList` is `mapStringList` is `mapIntListList`, all the way down to the compiler output. Unless you're doing some pretty serious performance optimization, giving them different implementations is a bug. And you wouldn't do so in a dynamic language either!
> non-trivial user defined types
These exist as part of your program structure whether you like them or not, the only question is whether they've been made explicit.
> Consider a simple program that adds 2 numbers: a & b together. In a dynamically typed language, this would be a + b. In a statically typed language, once you consider overflow and underflow, you will likely have a hundred lines of code.
Considering overflow and underflow in the static case but not the dynamic case is stacking the deck. Either you're using arbitrary-precision numbers or you're not.
Here's Haskell code for adding two numbers:
add :: Num x => x -> x -> x
add a b = a + b
If you don't want to worry about overflows, you use `Integer`s, which are arbitrary precision. If you're worried about performance, you use `Int`s, which are machine integers.
You can complain that this isn't "really" the addition code, since we're calling out to the `Num` instance, but the same applies to dynamic languages. Python's `a + b` is calling out to `__add__` in exactly the same way.
> This creeps up again in JSON parsing where the types really are defined at runtime.
They are not. The type of arbitrary JSON is (using Haskell again)
data JSON =
| JSONNull
| JSONBool Bool
| JSONNumber Double
| JSONString Text
| JSONArray [JSON]
| JSONObject (HashMap Text JSON)
(You don't have to use a hash map, of course: decoding the raw text stream to JSON is a separate step). You then check that it has the structure you expect by implementing a function `jsonToFoo :: JSON -> Either MyPreferredErrorType Foo`.
> Dynamically typed programs have much simpler structures and are much easier to reason about and test than their static typed counterparts.
That just makes no sense. Both programs have the same underlying structure, but it's hidden in dynamic languages. Having no compiler and type system makes reasoning much more difficult, there is no discussion about that.
> The larger the program you write with static typing, the worse it becomes compared to it's dynamically typed counterpart.
Again, that's just not the case: try refactoring Python code without type hints in a large code base. It takes a lot more time and effort and you can never be sure you caught all the cases affected by changes. The compiler tells you about every change needed to be made to arrive at a working state.
> I really wanted some hard research on this, but I know it's a hard one.
I think we'll have to settle for judgment calls.
I tried going through some of the research we have on developer productivity a few years back and it's almost all garbage or is only truly applicable to juniors (e.g. when you're new you really benefit from quick feedback time on static errors).
The entire space suffers from the fact that good experimental design is impossible to implement with anyone who's not a college student (good luck getting professionals to follow your rules for months) and the curse of dimensionality from the need to disentangle individual variations, type of software development, management style, and a thousand other things to try and draw out a signal.
Sadly, many things and life can't be effectively measured.
The article and many commenters here all talk about programmer ergonomics, productivity and "correctness".
The current research shows that these things are neither improved nor weakened by static typing. There are (even recent) papers on comparing typed vs dynamic language with no meaningful results. So basically it's entirely subjective.
However there _is_ an actual effect of static typing that can be trivially proven: It enables a programmer to write more efficient code. That should be at the forefront of every discussion around typing discipline, because everything else is just _hot air_ at this point.
TypeScript (used in the article) is an example that is _not_ actually strongly typed (it's statically typed but weak) and it doesn't even provide performance and memory layout guarantees, because the types are just comments. So you pay all of the cost of static typing without _any_ of the tangible benefits except documentation.
To me it is surprising that we as a technical community completely ignore actual evidence and take our cultural and personal preferences as fact.
> The current research shows that these things are neither improved nor weakened by static typing
I think there's a difference between "current research hasn't been able to conclude anything" and "current research shows [thing]".
I believe static typing is better, but that there's no conclusive research because there are many confounding variables and the experiments are really hard to conduct in the real world.
As other commenters have noted, the evidence here is really weak either way. The controlled studies are all done on college students, and therefore on pretty small short-lived projects. My instincts tell me that while static types may be optional at that scale, they are essential on large commercial projects that last for years, and projects like that are nearly impossible to properly study.
Other studies try to compare bug rates between open source projects, but it's hard to be confident in answers gleaned from that kind of observational study. As long as the evidence is so weak, the industry will continue to ignore the research and go with our instincts.
Also, many of these controlled studies must be taken with a grain of salt. Where the "grain" and the "salt" are the actual details, variables and boundaries of the study.
Aside from what you mention -longevity- many other variables will or can influence the outcome. Some extremes:
Juniors vs seniors vs mixed teams. Teams with many on-boarding and flying around between projects vs solo devs chugging along for decades on a single codebase. Using frameworks/architectures/design-principles vs yolo-architecture. No tech debt vs responsible tech debt vs crippling tech debt. Agencies who only deliver (i.e. the throw-it-over-the-wall business) vs teams that must maintain; for decades.
I'm quite certain that if you are an agency that builds websites for small businesses, wordpress (PHP) and/or react (JS) with bazillion libs just glued together in a "works here" manner will give the highest ROI: you don't have to maintain, upgrade, scale, secure, etc it anyway. Statically typed languages will harm their business (but, I'm convinced, will benefit their customers and our industry at large) because that's: deliver fast&cheap at the cost of all else. I'm certain, because I helped such agencies improve their ROI. "YOLO" turned out to be the most important factor to increase revenue (on short term!).
I'm certain that if you are a startup who must onboard new hires because your are rapidly growing, having solid and enforced guidelines and guardrails in place helps a lot. One such guardrail is a typing system. (I'm certain because I've been there, we didn't have any: it took new hires month(s) to have their first PR merged)
I'm certain that if you have mixed teams where the seniors can get juniors unstuck, and where seniors can design and build the type systems (bounded domains, value-objects) the architecture (e.g. outside layers/adapters that ensure everything within is guaranteed typed) the juniors are far more productive too. I'm certain because I've been in this role and we saw productivity improve drastically.
etc. Such controlled studies are not only hard to do, they need to be read with even more care: did they study a situation or control for my cases at all?
Static typing is just one slice of Swiss cheese in the pursuit of more reliable software. Yes, it will leave holes like any other technique, which is why you should mix and match them for maximum reliability. But throwing out static typing because it doesn't catch everything is like choosing to leave the door unlocked because the thief could always break a window. If security is really important to you, then you should have bars on the windows in addition to locking the door, not instead.
It looks like you think writing your program in the language used to express types will magically remove "runtime bugs"
If the language is powerful enough to be able to write ordinary programs, it is powerful enough to produce bugs.
Static typing can be effective at catching certain types of bug, not all. It can improve readability sometimes (as a DSL for static unit tests/executable docs).
In general, dynamic languages are more agile and you can write more tests easier. Some of the tests you wouldn't need to write in a statically typed language, therefore typing is still useful though not as universally effective as one might believe.
> In general, dynamic languages are more agile and you can write more tests easier.
Where's the evidence of this? Be wary of speaking from "common sense", this kind of assertions are deceptive. Maybe statically typed languages are better prototyping languages? (E.g. there's evidence from an early experiment by Paul Hudak that Haskell is actually a good and fast language for prototyping, beating other languages in the experiment!).
This is more of a reflection on tsc, but also, Hello World is not a good example because it's not a real world prototype.
For conclusions about rapid prototyping you'd need to do this experiment:
- Prototype something real, not a tiny toy example like "Hello, world".
- Go end to end with your experiment, so instead of focusing on how long it takes to run the typechecker (which makes no sense, since dynamic languages don't have this step), measure how long it takes you to iterate your prototype from start to a finish (including everything: failed attempts, code does not do what you want, writing tests, troubleshooting stuff, and the final working demo).
Again, I must ask: where is the evidence that dynamic languages are faster for prototyping?
This paper by Paul Hudak, "An Experiment in Software Prototyping Productivity" [1] has multiple flaws you could argue, but what's amazing is that Haskell beat all other languages in the list (none under discussion here, sadly -- that's one of the flaws) for fast prototyping!
Why do people keep using hello world as good examples of anything?
I'm not a TS zealot at all, but my work project which is around 2000 TS/Vue files (some of them hundreds of lines long and even breaking into the thousands for a lot of them) manages to get checked in around 3 seconds if there's a cache, and no-cache runs take 10s or so (which also involves actually transpiling the files)
The parent was looking for evidence for why someone could write tests easier with dynamic languages. A long waiting time for type checking to finish came to my mind first. I think hello world is a valid example when talking about unit tests.
I don't think Hello World is a good example for anything; all you can do with it is microbenchmark, which is not particularly interesting. There are also no meaningful unit tests to write with Hello World.
With tiny examples, compile/tooling times tend to dominate.
More interesting would be a larger, non-toy prototype, where the tests and prototyping effort are meaningful.
> The research out there found no meaningful difference between both styles
I don't know which research you're thinking about, but if it's the one I've seen, it was empirical analysis and it's worthless because it's subject to survivor bias: whatever the tool you're using to make a program, it's going to be polished just as much as needed for it to fit its market, or it will die, simple as that.
If one paradigm lead to much more bugs per written line of code, but you're using it anyway for mission critical stuff, then you'll fix those bugs or fail. On the other hand, if one other paradigm leads to very little bugs compared to others, but you're using it somewhere where reliability doesn't matter, then the few bugs that are there from the beginning will stay there.
We can even call that the iron rule of bugs: No matter what tools you use, your code will contain roughly as many bugs as your business can tolerate.
The real question is how much effort does it take to achieve the same level, but it's obviously harder to study.
> The research out there found no meaningful difference between both styles (unless there's newer research I haven't seen?) and people keep taunting around how their preferred side is undoubtedly the right one.
Does it, though? I'm not sure the research shows any strong evidence one way or the other. There's a decent review of various studies here, for example: https://danluu.com/empirical-pl/, that seem to indicate no real conclusion.
Perhaps the issue is how people like to solve problems. I'm a huge typescript fan, and I say a lot of the hard work is designing the types. Then the code becomes 'the only thing that makes sense given the types'. There may be some duality at play, where otherwise designing the code is the hard work. And the types become, 'anything that makes sense given the code'
I had to learn TS because that's the hype today, and went into it after seeing some amazing F# talks on typing. I was amazed at the way they used types to ensure your code was actually correct. I was excited to be able to do that with TS.
Then I found out the hard way that none of that works. Basically it's all a lie. You can't use the types in runtime so all that effort you put into the types doesn't actually translate when you want to use the type system to full effect. That was absolutely demoralizing.
For me, the white elephant in the room is that the language doesn't really matter all that much. Good developers will write good code and bad developers will write bad code.
Good developers might use meaningful types and bad developers will use strings, records and numbers everywhere. Guaranteeing a string is passed and not a number is not gonna prevent many bugs. Guaranteeing a `PhoneNumber` is passed can truly prevent bugs. But that's never the code that you actually see in the wild (even in the article).
In the real world, most people can't even use half the type system they claim is so great.
> You can't use the types in runtime so all that effort you put into the types doesn't actually translate when you want to use the type system to full effect.
This isn't really the problem: runtime types would solve the issue, but they're a sledgehammer with all sorts of nasty effects elsewhere. Languages with far stronger type systems than F# still have type erasure: it's actually fairly unusual in that regard.
The core weakness of TS (aside from the fact that its "type" system isn't actually sound) is that it doesn't have type-directed emit: you can't automatically generate safe parsers, for instance, because you can't generate anything. Except enums: why those got a pass is beyond me.
Oh, you definitely need something like io-ts, runtypes, zod, there's a few more, to do runtime validation of data.
Zod I think is quite popular.
Typescript isn't perfect (sound?) So you can still get errors even if things type check, but if you are disciplined using those libraries instead of `as`, the problems are minimal.
type MyPolymorphicType = { type: 'A', value: number } | { type: 'B', value: string }
There you have types at runtime. If you really want to be pedantic about it you can use classes and instanceof as well
In fact even Java removes types at runtime for code that uses generics, so often you need to do something like that as well
Types as a static analysis tool is great, types tied to the underlying hardware and memory structures have their values (usually performance benefits). Conflating both together usually leaves you with a very weak (unsafe) type system.
That's using values to determine different types. What I'm talking about is true type validation where I can detect differences between the same data.
TypeA = number
TypeB = number
const myNumber = someFunctionThatReturnsTypeAorB()
I cannot tell which number I'm dealing with because TS doesn't know that type at runtime. I don't think this is about being pedantic. If the language forces you to change the data structure to allow you to differentiate types at runtime, then it's a very limited type system IMO.
If all of this came for free, I wouldn't argue with it. But people tend to disregard all of the cost that comes with it. I know I have lost countless hours simply changing code that already works to make TypeScript happy. If all that is to tell me I shouldn't use a string in a number argument, I don't know if the benefits outweigh the cost.
Research is... complicated. I've seen a lot of research, some shows that types prevent bugs, some doesn't. It's all over the place. If you really want me to I can find you some papers. I've read perhaps two dozen, total, maybe 15+, dunno. The methodologies range from "we scanned github" to "we went to a company and had them solve a problem twice" to "we took a curated group of developers, trained them in two made up languages (one with and one without a type system) and let them attempt to solve a complex business problem" - and the results vary, although my recollection is that they are largely "pro types".
The thing is, not everything is going to show up in research results. There are an insane number of variables.
How do you account for developer experience? How do you account for experience within a ___domain? Experience with a language? How do you account for different type systems? Training with type systems? Complexity of the ___domain? How do you account for one developer being better than another? How do you account for development methodologies? How do you account for people's moods that day? How do you account for effort?
It's just not something you can easily research. It's extremely expensive just to generate a small study that tries to control for some of these things.
In the meantime, we can use our experience as engineers. We have to make a judgment. We have to form opinions that are biased and subjective.
That's because there's no recorded metric for every time a dev working in plaint JS mis-spells a property, or accesses one at the wrong level, calls a function that doesn't exist on that particular version of the library, etc, etc.
For the most part, the resistance to Typescript from devs who prefer to write plain Javascript is purely borne out of adding the types being "too much effort to be worth it". Sure you gotta wrestle with the type system now and then but there's often a reason you have to + once it works it usually works very well. Which I find kind of funny bc those devs don't mind putting the effort into writing unit tests, but adding types (think of them as just lower level tests) is somehow an insult to the way they do things.
But nah they'll still just rawdog their JS and just make mistakes/misspell stuff now & then as we all do. Let's just hope tests/CI pipeline is enough to pick it up before it gets to prod.
> Language design does have a significant, but modest effect on software quality. Most notably, it does appear that disallowing type confusion is modestly better than allowing it, and among functional languages, static typing is also somewhat better than dynamic typing. We also find that functional languages are somewhat better than procedural languages.
> The languages with the strongest positive coefficients - meaning associated with a greater number of defect fixes are C++, C, and Objective-C, also PHP and Python. On the other hand, Clojure, Haskell, Ruby and Scala all have significant negative coefficients implying that these languages are less likely than average to result in defect fixing commits.
> The data indicates that functional languages are better than procedural languages; it suggests that disallowing implicit type conversion is better than allowing it; that static typing is better than dynamic; and that managed memory usage is better than unmanaged.
Regarding your comments:
> Personally I like typed languages but the type systems in languages like TypeScript are simply insufficient. You still end up with runtime bugs because you can't actually use those types at runtime.
That's because TypeScript is not actually strongly typed, it is gradually typed; throw in a single "any" type into your TS and all static guarantees are now off. I agree that TypeScript is insufficient, and point to languages like Rust or Haskell (one of the languages with the lowest defect rate in the study [0]) that actually do offer static guarantees, and where use of untyped "escape hatches" is far less common and/or is far more judiciously applied.
> If a type system could basically remove the need for me to think about runtime bugs, then that's an absolute killer feature that I doubt anyone would argue against. But most languages don't provide that
Isn't this basically the promise of Haskell, Rust, et al? "If it compiles, it works;" "guaranteed memory safety," etc?
> Isn't this basically the promise of Haskell, Rust, et al? "If it compiles, it works;" "guaranteed memory safety," etc?
Yes, 100%. If that's what people were referring to when they argue for typed languages, then I'm all for it!
But the article (and almost everyone else that brings this up) then tries to use TypeScript as an example. That's a big issue for me. TS leaves you hanging in the middle with a ton of boilerplate and you fighting the imperfect type system, while catching very few bugs (at least for an experienced developer).
I don’t think you need a “study” for example when Elm has no runtime exceptions. That’s an entire class of bugs plain impossible to express (bar a few contrived examples)
I know that, its what I meant with bar a few contrived examples. In practice you dont really run into them like youd run into runtime issues in Javascript.
Elm is better, but I would still recommend against it.
- The dictator for life is ambiguously benevolent at best
- JS interop is deliberately heavily locked down (good) with no escape hatches (bad)
- the compiler release cycle is extremely long: there hasn't even been a minor version released since 2019.
- Elm generally picks good defaults, but often makes them impossible to opt out of. For example: it stops you from having implicit effects in your render code by making it impossible to do effectful rendering at all.
- It's optimized for making people with no functional programming experience into competent beginners, but the ceiling is low. You will eventually want to jump to something more powerful (Purescript if you need a production-ready compiler, or GHCJS-Haskell if you don't and are feeling adventurous), and when you do the process of swapping out all your libraries is not going to be trivial.
Good points, thanks for that. I'm more the old-school, server-side web guy from CGI and servlet days, who happens to like Haskell and related languages now. To that end, I haven't really been doing anything front-end at all, but would like to explore some of that.
I'm not even sure that HTMX isn't enough for my needs, so smaller and lighter is my definite preference. I suppose I should try more Elm now, and tinker with Purescript and GHCJS later. If it's all side projects, then I'm not really swapping anything unless it's a choice I'm making to explore new-to-me tech.
It is pretty much impossible to prove this one way or another. I also believe it varies from person to person. I am personally way more productive using typed languages. However others claim the opposite. And we might all be right.
The social pressure to love static typing notwithstanding (think less of me if you will), the reason I got turned off by it eventually are the ivory towers that invariably get built up around them. I've spent a decade each building software in both paradigms and I now prefer not to use type systems.
I find dynamic typing similar to how unit testing is a forcing function for writing composable code, it acts as a forcing function to writing simple code; simple to read and simple to comprehend.
The argument that it helps inexperienced developers approach the codebase is, IMO, a poor one as it tends to incentivize an iteration loop that lacks understanding and is simply trying to make all the red go away. In fact I find that the type system is often a barrier to truly understanding what's going on as it effectively adds a very ___domain specific language to every project that has to be learned on top of the language itself.
There are methods to solving the problems presented in TFA that are just as robust as using types and which are simple to understand. Can types be used in a simple way? Sure. Are they ever? Not in my experience. I also don't like autocomplete, so take that as you will.
I may just be a grizzled greybeard screaming "the code _is_ the documentation", but perhaps that is born from my deep dissatisfaction with the current breed of get-big-paycheck-chatgpt-said-it's-right devs that are currently flooding the industry.
> I find dynamic typing similar to how unit testing is a forcing function for writing composable code, it acts as a forcing function to writing simple code; simple to read and simple to comprehend.
If this were broadly true for most developers using dynamically typed languages, it would be a very compelling argument indeed. But I think there’s pretty strong evidence the opposite is generally true: actual types produced to document real world dynamic code tend to be vastly more complex than the equivalent functionality implemented with static types from the outset. In the TypeScript ecosystem, DefinitelyTyped is an excellent source of countless case studies. The types they provide are typically definitely not, as you put it, “used in a simple way”. That’s not complexity inherent to the type system, or the type definitions as provided; it’s complexity inherent to the dynamic code they describe.
Equivalent packages which were statically typed from the outset tend to have much simpler interfaces because the types are defined upfront rather than retconned onto existing APIs. That doesn’t necessarily mean their interfaces are absolutely simple, but they’re typically relatively simple by comparison.
I’d go so far as to say that you can’t know how simple or complex an interface is without specifying it. If “the code is the documentation” (which I agree is a great ideal!), then interfaces without code specifying them are under-documented by definition. The further the code specifying the interface is from the interface itself, the more obscured your documentation is.
Dynamically typed language have very clear rules. It's not magic. If you know these rules nothing about dynamically typed code is inherently problematic. It's the difference between logic being in the code versus in the run time. You accept hundreds of rule sets built into the run time all the time.
All programming languages - all computers even - have very clear rules. The complexity in software development usually has less to do with getting a computer to understand its own rules, and more to do with the boundaries between humans and those rules: for example, encoding human ideas into rigid computer rules, or trying as a human to understand the computer rules that someone else has written.
So when you say "if you know these rules nothing about dynamically typed code is inherently problematic", my intuition says "I probably don't know these rules". That goes for some of the basic rules like "the first argument to this function is the haystack, the second is the needle", but it also goes from some of the more complex rules like "functions that take a user ID can also take a user object and extract the ID from that" or "the allowed states for this FSM are X, Y, and Z, and are always written in capital letters". More importantly, a lot of the rules for my difference will not have been written by me, they'll have been written by my colleagues, or else they were written by me, but it was more than six months ago and my memory is a bit hazy on the details.
The point the previous poster was making, I think, was that while the rules may be very explicit (this is, after all, what any programming language is: explicit rules for a computer to follow), they can also be very complex. And, more specifically, the DefinitelyTyped examples show that the rules for dynamic software often tend to be very complex, or at least, complex enough to present difficulties when being modelled by a type system explicitly designed to model dynamic code.
> But I think there’s pretty strong evidence the opposite is generally true
I could not find good research that will decisively settle this dispute about static vs dynamic. So when you say "strong evidence" what are the sources for this strong evidence? Not asking to push against, I am gathering a list of articles and papers about this topic.
I’m only responding to the claim that dynamic types act as a forcing function to produce simpler code, not the static vs dynamic distinction more broadly.
It’s possible there are papers on the topic, but I’m not aware of them. The evidence I’m speaking to is the complexity of the respective types themselves, ie either:
- those produced in the course of developing a given software package with static types as an explicit part of the development process, versus
- those produced post hoc to document the apparent interfaces of an equivalent package which has been produced without explicit types
One might call foul, by saying that this criterion favors static types by evaluating the complexity in terms of static types. But the reason I think it’s a fair comparison is because the dynamic interface does have a static type representation, even if it has to be conjured into notation post hoc… in much the same way as a data store model does have a schema even if it’s not formally specified as such.
> The argument that it helps inexperienced developers approach the codebase is, IMO, a poor one as it tends to incentivize an iteration loop that lacks understanding and is simply trying to make all the red go away. In fact I find that the type system is often a barrier to truly understanding what's going on as it effectively adds a very ___domain specific language to every project that has to be learned on top of the language itself.
This is very interesting and at first blush strikes me as backward. I wish I could sit in your office and see this happening, because I find it hard to imagine and I would love to know what that looks like. (And I have way less experience than you so I'm not saying it can't happen.)
Specifically, I find the ___domain-specific logic in my field is impossible to grok in dynamically typed codebases, while statically typed ones actually teach the developer what the business logic is.
> I may just be a grizzled greybeard screaming "the code _is_ the documentation"
Also confusing to me! In all my experience, static typing is what allows the code to be the documentation. Without it, there's no way to know what properties this object has or why we have a check for this certain property that I thought didn't even exist on this object. (Other than comments - maybe I'm on a weird team but I don't know anyone other than me who leaves comments of any significance.)
Comments are great, but they should usually explain why, not what.
Good, long variable and function names, along with breaking complex expressions into multiple statements with those nice variables names can help the code itself describe the what of the process. And then static types help describe the what of the types of data even more.
That lets you save the comments for more useful things than an ad-hoc type system, like "// We need to do this because..."
I really don't understand these lines of argument, because they seem to me to be almost entirely backwards.
> I find dynamic typing ... acts as a forcing function to writing simple code; simple to read and simple to comprehend.
I find the opposite true. Many super-dynamic patterns are hard to type correctly. Good type systems tend to encourage simpler patters so you get simpler types.
> The argument that it helps inexperienced developers approach the codebase is, IMO, a poor one as it tends to incentivize an iteration loop that lacks understanding and is simply trying to make all the red go away.
Making the red go away is important because the red indicates a problem! This is a lot easier than other ways of discovering the error. Why would you want to discover the error later?
> I also don't like autocomplete, so take that as you will.
This is why I'm with commenters who say they don't trust people who are against static typing... I'm extremely suspect of computer programmers who don't want computers to help them program.
> Making the red go away is important because the red indicates a problem! This is a lot easier than other ways of discovering the error. Why would you want to discover the error later?
I don't think that's what the parent to your comment is arguing. They are arguing that "making the red go away" isn't the goal, rather that correctness is, and that it's easy to conflate the too when you focus too much on the "red" part, and don't pay attention to the "correct" part.
Worded another way, the mantra of "if it compiles it works" can lead to a dangerous false sense of security if you don't understand the limitations of your type system and what parts of your program is may or may not cover completely.
Very few people have this mantra, much less without qualification. That's more of a ML or Haskell kind of point of view, and even then it's known to not be a guarantee. A type Int -> Int -> Int isn't going to enforce that you implement multiplication correctly, instead of add or just 0.
"I refactored, fixed type errors, and it just worked!" is a thing I see a lot, but from people who just experienced it, because it happens a lot. It's a good thing.
While discussing code review, howabout shooting whoever wrote that comment.
Why is there an assert? Python case a cast function if you really wanted to force the typechecker to see a_thing as Thing but (and I'm sure this is the point you are making) you are likely hiding a prolem.
I think you make a good point about the ivory towers and the social pressure being a turnoff for adopting certain technologies. Though I also think this doesn't detract from their potential technical merits. As in, a technology can be both great, and have everyone about it be pretentious.
> I find dynamic typing similar to how unit testing is a forcing function for writing composable code, it acts as a forcing function to writing simple code; simple to read and simple to comprehend.
I don't agree with this argument, I think it's akin to saying "I like driving blindfold because it makes me drive slower". You can use linters with limits on line length and number of params if this is a goal for you, no need to go indirect with the restrictions.
I agree that typing is not the only solution, I'm just saying that I think the ROI is massive in types, so I think this should be the first tool people go to. Almost no investment, and a lot of benefit.
I agree with "code is the documentation", I even made a point about documentation in the post, but I argue that typing is part of the code. So "the code is the documentation, and typing is part of the code" is how I'd phrase it.
> > I find dynamic typing similar to how unit testing is a forcing function for writing composable code, it acts as a forcing function to writing simple code; simple to read and simple to comprehend.
> I don't agree with this argument, I think it's akin to saying "I like driving blindfold because it makes me drive slower". You can use linters with limits on line length and number of params if this is a goal for you, no need to go indirect with the restrictions.
I was assuming this is more along the lines of writing clear variables names and writing functions that make it obvious what the return value is.
I was going to write a response pretty much exactly like yours.
Also, the ___domain we're in is engineering. There are no single correct decisions and everything is about trade-offs. And that's good because otherwise our jobs would be the first to be automated out of existence. All of the discussion in this thread about people "thinking less of" other engineers for their opinions/experiences is fucking gross.
There's a chasm of difference between blatantly self-destructive life choices and forgetting that your profession is about making decisions based on multiple options.
Also you only half-quoted me.
It's "... and/or on drugs".
Also people being stupid doesn't mean that I don't consider anything they say. Even a broken clock is right twice a day.
I'm fully a convert to static typing and don't think I'll ever willingly do another project without it, but you make a really good point about how people tend to always go overboard with types. I did this early on in a large TypeScript codebase while learning TypeScript, and now I regret it. I'm still far better off than I'd be without static types though.
This also happens with tests. It's easy to get wrapped in testing libraries and abstractions and spend way more time than you need to.
The 80/20 rule applies in both areas. You get 80% of the benefit with 20% of the abstraction. It's usually a mistake to try to go beyond this.
Of any popular language I'm familiar with, I'd say Go is the one that follows the 80/20 rule best. It encourages you to focus on solving the problem rather than leading you down rabbit holes of various kinds, including with types. That's not to say Go doesn't have its issues, but it really is excellent at discouraging over-engineering.
You must have a crazy good memory. I switch between multiple languages, and autocomplete lets me easily see what variant of a function this library uses again.
I also hate autocomplete. It just gets in my way. Autocomplete doesn't really work unless you already know what you're going to code, and if you already know what you're going to write the autocomplete prompt just gets in the way and often messes with keyboard entry of what I was typing. I just switch it off, it never saved me any time and just gets kind of annoying.
> Autocomplete doesn't really work unless you already know what you're going to code
Say what? Autocomplete is a godsend for quickly working with new APIs. If you're writing a function that takes some objects that you're not familiar with autocomplete can pretty quickly lead you down a good path.
That's not my experience. If I'm working with new APIs, I have the docs up on one screen and the code on the other, and I've probably already copied and pasted the function and parameters into my code from the docs before autocomplete can get in my way. You do read the docs, don't you?
I also don't like autocomplete. The reason being that not having it actually forces me to learn the libraries I am using.
> You know everything by heart?
You eventually reach that point. Do you still look down to see which keys you are typing?
This is also has the nice side effect of pushing you to use libraries that are stable and have good documentation as you can always reference them if need be.
My memory is a leaking bucket. My typing however, is very fast.
Take for example the substring function. I wouldn't know which one it is for JavaScript, Haxe, ActionScript3, C#, Java, Python, C, C++, PHP. I know I used it at one point for all of them.
For languages like JavaScript, Java and C#, it's probably a method, so you can start typing. Might be substring(), might also be substr() or something like that.
For Haxe, I thought it was not a method so I think it's either part of Std or StringTools.
If I use autocomplete, I have it in a few seconds, and I can also see the documentation on the parameters. Is the second parameter an index or a length? To be honest I have no idea. And the good part is, thanks to autocomplete I don't need to know.
Also, if you work in a big codebase, you can't know every class. Needing to dig through code seems such a waste of time.
> For languages like JavaScript, Java and C#, it's probably a method, so you can start typing. Might be substring(), might also be substr() or something like that.
In JavaScript, there are both, and a third one besides, largely for reasons of historical accumulation and inconsistent implementation:
They’ve each got their strange nuances in behaviour (and, in so-ancient-you-certainly-don’t-care-about-them engines, cross-engine inconsistencies), so if you’re relying on autocomplete, it’s necessary that your autocomplete at the very least give some meaningful parameter names, because the difference between taking length and an end index is rather significant.
Which one should you use? slice. It’s generally agreed to be the most reasonable of the three in what it does and how it works, and it matches Array.prototype.slice well. So: sorry that you thought it might be substring or substr, because those exist but you probably shouldn’t use them.
This is because you can remember where the keys are.
> For Haxe, I thought it was not a method so I think it's either part of Std or StringTools.
How would autocomplete help you here? If you need to know if it was a global function or else where? This is learned knowledge that is available through the proper docs and not through autocomplete. This is how you know not to look for a global function.
The difference is that instead of reaching for autocomplete to try and fill in the gap, I go to the documentation. I basically share the same feelings/experience with this comment[0]. Either I know what I am doing and I do not need autocomplete, or I need to learn in which case autocomplete just gets in my way and I would rather go to the docs.
I know I am not alone in this, for example I watch the lead developer of pidgin, Gary Kramlich, on twitch[1]. He does not use autocomplete, any time he has to look something up, its straight to the docs.
Similar with Jonathon blow[2], who is working on his own game, game engine, and language who just uses emacs (with no autocomplete) and visual studio for the debugger.
And even Mitchell Hashimoto[3], who has a similar workflow of using a dumb editor and then going to the docs when needing to learn.
> I can also see the documentation on the parameters. Is the second parameter an index or a length?
The documentation on a per function basis does not contain enough information on how to use the library/framework. Take for example django's QuerySet aggregate function, would you learn enough from reading the function documentation here[4] on how to use it or from the actual documentation here[5]? Autocomplete wont give you anything close to the latter.
> Also, if you work in a big codebase, you can't know every class.
The classes you don't know are just one "goto definition" away.
> Needing to dig through code seems such a waste of time.
Needing to juggle through the autocomplete menu is a waste of time as you need to know _something_ for it be useful. It doesn't help you when you know _nothing_. I'd rather just go straight to the documentation or code.
> This is because you can remember where the keys are.
No I don't remember, it's automated, like riding a bike. (Edit: for example if I need to tell you where a certain key is, I cannot bring it up from memory, I have to virtually type it in my head with my fingers, and then I know. So this really shows it's really muscle memory)
> How would autocomplete help you here?
Worst case scenario, I type "object.subs", wrong "Std.subs", wrong "StringTools.subs". Faster than you can bring up any doc.
> Either I know what I am doing and I do not need autocomplete, or I need to learn in which case autocomplete just gets in my way and I would rather go to the docs.
What about when you partly know, like substring?
Plus, how does it "get in the way?" You partly type it, and when you see it, you just press space or whatever completes it. Even in typing it's faster, because a good autofill even uses word distances for matching.
> who just uses emacs
There's the problem! ;)
> Needing to juggle through the autocomplete menu is a waste of time as you need to know _something_ for it be useful.
This is exactly it. Most of the time, you know something or at least can take a quick guess. If you fail there are still the docs. You and the other person are very explicit in "either you know it or you don't know it". For me, most of the time, I kind of know it. Probably the main difference is there.
Anyway, good thing nobody is forcing us to use anything, so we can both be happy in our own workflow :).
One question though: have you used Github Copilot and what do you feel about that? It would seem to me you would also feel it gets in your way.
My working memory has limits. I consume and use a lot of information on an ongoing basis. The contextual shift from codebase to codebase is already large, add in a language change and I would venture to say that most people need a little assistance to make sure they remember syntax and specific method calls.
> The argument that it helps inexperienced developers approach the codebase is, IMO, a poor one as it tends to incentivize an iteration loop that lacks understanding and is simply trying to make all the red go away. In fact I find that the type system is often a barrier to truly understanding what's going on as it effectively adds a very ___domain specific language to every project that has to be learned on top of the language itself.
What? It helps developers inexperienced with a given codebase (not necessarily inexperienced generally), because you can very easily understand the types being passed into functions. You may need to understand why a function is behaving a certain way, so you look at the arguments, if you see a type you don't recognize you can push a button and your IDE will take you to the definition of that type. If you want to you can with another key combination see all the inheritors of that type, and so on. It makes it much easier to feel confident about what one can and can't do with any given object at any given time, which is helpful when one is getting up to speed with an existing breed of architecture.
The social pressure side, I totally get. People get very defensive about this sort of thing, and act somehow personally offended if anyone does the opposite.
I say that as someone that quite likes static types. I don’t care, do what you do!
I had this attitude too until I got sucked into a very large Python project. I now refuse to use Python for anything bigger than a few pages of code. It's so poorly suited to correctness that it takes an enormous amount of test coverage to gain any confidence. And don't get me started on refactoring. After this experience, I suddenly care a lot more what other people use, because it may impact me in the future. Please don't use such languages for big complex "must be correct" problems.
> I now refuse to use Python for anything bigger than a few pages of code.
Your comment pretty much echoes my opinion. Unfortunately in the problem space I work (data) Python is pervasive, but fortunately, it can be used in small isolated snippets, or Pyspark where this doesn't matter much.
I think the social pressure relates to people forcing _you_ to use a language that you wouldn't personally go with. Pretty sure the pressure is highest with TypeScript currently.
Code can not be documentation by meer definition. Code is written in programming language and documentation in spoken language. So documentation has important function of explaining intention of code in non trivial sections. Of course you wont give explanation of CRUD actions or other patterns you are using but business rules get coded and people reading that code need to know where rules come from and what are expectations either directly in code or by reference. Otherwise you revert to finding origin of code in source control and related task if you are lucky to have that level of tracking.
Dynamic types do make people create more dynamic APIs because it is a lot easier to let the parameter be number or string or format-function or bigint, etc
Dynamic APIs are super hard to reason about if you don't have types to ensure you are calling your APIs correctly (function argument A can be number or string, but not formatFunction)
So having static types makes your APIs less dynamic because people are too lazy to type them correctly. Which in general is good in my opinion, making an API dynamic should be done only when it is really helpful to do so.
The other point is null-guards which _some_ (but not all) type systems enforce, null-guards are just 100% good thing.
When I hit ctrl+save, the code should be instantly ready to test, less than 200ms. Waiting for 30 seconds to see code is an ETERNITY to people that code fast, 1k lines per day. I have been on teams that wait 5m to test code, what a waste. Fast feedback loops in code are critical, just like design, or anything important. Types seem like some weird religion, the people that use them cannot be talked out of it.
30+ second compile times is definitely not something inherent to statically typed languages. 200ms is rare, but plenty of static languages manage <5s for large projects and much less for small ones. Not to mention that a good IDE will incrementally check types in well under a second as you type, so the need for frequent test runs is lower.
That said, I agree 100% about the importance of fast feedback loops.
I highly recommend you to check out ReScript. Amazingly fast compile times.
Watching typescript teams spent 30 seconds on the recompile is insane for me.
in fact, the whole ocaml family of programming languages are mega fast.
The most important feature of a very fast compile time is that you can load it with types after types and write in your whole mental model, without worrying about your build process slowing down.
in my experience dynamically typed languages are slower to compile than dynamic ones. groovy, for example, is massively slower to compile than java (it is something like a factor of 10x in our codebase), and the whole point is literally to give you python-style code velocity.
that's not to say that you can't write slow statically-typed compilers. but the strict syntax tree gives you much faster parsing/etc. dynamic languages can't avoid this either, it just does it at runtime instead.
and hot-reload for function changes/etc is very possible in a similar fashion to dynamic compilers too. And if you add a space or a bracket, because of the parsing problem, you can't even notionally contain that to a single part of the AST, the whole thing has to be re-parsed.
again, not that you can't make a static language compile very slowly, or that any given implementation is necessarily faster. but dynamic languages almost strictly have to do more parsing work, and have to do it more often, over larger parts of the codebase. and that's expected - static languages offload some of this "preprocessing" to the developer!
Strong static typing, where most of your data is going across the wire as JSON, is a battle that is largely fought in incoherent ways.
Yes, you should try and use all tools at your disposal. But you should also find that most "data" is far more squishy than you think it is. People don't fall back to phone numbers being strings because they are lazy, they do it because too many of them made the mistake of thinking they could make them a stronger type at some point. Same for names. Or addresses. Or postal codes. All of these things need to be taken from a user, and the only real way we have to do that is by parsing text. And if you make the mistake of making your system so it doesn't store the preparsed text, you are almost certainly going to regret it at some point.
Now, is it best to have a layer that keeps the original user entered text and offers it as a typed set of data to the user in the backend? I certainly think so, but there will be ROI considerations as to how much that matters for your little part.
More, if you are doing evaluations of any heavy sort, you probably want a translation layer to a SAT or other numerical model for calculation. And in that world, the numbers are the abstraction. Trying to do it another way will almost certainly lead to pain. And again, you will do well to have translations from problem to formulation, and from solution space to ___domain. All of these can largely be helped with types, but far too often the "types" that are focused on are not these.
Author here. One thing that we do at Svix that I alluded to in one paragraph but I should probably have elaborated on further: thanks to libraries like Serde and Pydantic, we actually follow deserialization is validation (is that a term?), which means that we validate all of the JSON data before even creating the structures in our code.
I guess that's similar to the redis example I gave, but it essentially means that even though we get sent JSON over the wire, we validate it fully and when it gets to our code we know it's a well formatted type. So our code can assume an email type is a valid email, an ID type is a valid ID, etc.
What I like that you're doing and what I used to do with Python annotations is that you strongly define types for your higher level business logic - which may be encode quite complex formats.
In many ways, your point is correct and kind of difficult to argue against (i.e. I agree!).
What catches alot of people is they say how important types are but then just use 3 or 4 primitive types and never build custom types on top of them so they don't get much value out of those types aside from preventing the absolute worst runtime errors. The real value of types is having lots of them and evolving them over time as your systems and capabilities mature (US street address, US state, latitude, ... the list goes on for any piece of software).
Huh! That's given me a lot of insight into why I've had to argue about typing in the past. Thanks for encapsulating that for me.
I've had teammates "agree" and use types only to use the most basic possible types and then confuse themselves later (like, days later). But to me, because I like static typing, it comes very naturally to define types based on business logic, which makes things very easy to work with. And they're just not thinking in that way. Now I wonder how I can bring them around.
Right, but you should also take pains to keep the original. All too often the deserialization/validation can also alter the data. In many places, this is fine, I think. In many others, it leads to a ton of confusion.
Edit: I wrote "but you should", I really should have worded that as "and you should". I am wanting to add to your point, not contradict it.
Oh yeah, I agree, and I learned it the hard way. In many cases you want to keep the customer format rather than the canonical format. This is why I stopped using Postgres's JSONB for customer data, and exclusively use String/JSON nowadays.
In my opinion this is the single biggest benefit of GraphQL, forced validation of data server-side. Sure you can do that with swagger in REST apis as well but nobody ever bothers to do it strictly and correctly.
Technically you can have more complex and custom validation (email is a valid email address) with annotations but the client-side will not have this information.
This seems to be a common practice in Go and I've seen it cause requests to be rejected because one unimportant field is wrong. Were we to do this for truly important requests, like lead traffic from third parties, we would lose a lot of business.
This is correct - if it's going to error or panic, it should do it at the boundary, not at a random place in the program. It's also much faster to debug where the issue is this way.
You should still use name and address as types not strings, even though when you look deep you discover they are both strings. It is [almost?] always an error to mix up name and address fields and the type system can enforce this.
This is why I said it will be an ROI of where you are. In your javascript just pulling the data out of form fields to send to the backend? Probably not worth it. Somewhere between taking it from the user and processing it? Probably worth it. And sending it to another system? You almost certainly need a translation between your types and theirs. But making a layer that converts between every one of your types to every possible external system? Probably not worth it.
Not to most of the debates you will see online. For one, they will be quick to throw that json is closer to Map<String, JsonAtom>, where JsonAtom can be JsonNumber, JsonArray, String, or Map<String, JsonAtom>. Then the clojure/lisp crowd will start to ask why can't they use a myriad of long standing tools that work on lists with this data. (And for more fun, the lisp crowd will point out that json is closer to an A-LIST, not a Map, since they allow duplicate keys... At this point, most of the crowd will rightfully tell us to shut it. :D)
Though, to be clear, I think I largely agree with you. I don't necessarily think typing is a curse. I just don't think most of the typing debates we see online are really telling us much. I'm reminded of a while back when everyone was writing what a TODO app would look like with their favorite abstractions. When, realistically, all applications get dirty after you throw enough users and existing data at them.
Yeah we are on the same page, lol. Practical usage isn't as sexy as diehard ideological arguments.
That said, what JSON corresponds to doesn't matter -- you're parsing it into a data structure that makes sense for your use case. You do not need to represent arbitrary JSON, and should not -- only valid values. So maybe that means something like
struct Person {
String name;
int age;
enum Country birthplace;
Map<String,String> other attributes;
}
I am baffled by the purists who act like this is somehow not allowed.
Or if you genuinely are just building a generic JSON parsing library, that's easy enough as you described. The pseudo BNF definition of JSON is simply mapped directly to sum and product types. I have worked on (very lightly) a popular java Json library (Jackson) and it was no problem...
Another problem is lately there are novice people whose only exposure to type systems is, of all things, typescript (or pep484 python) which is a horrible example of a practical type system because (much like pep484) it's been bolted on poorly onto an existing typeless language.
You're not "moving faster" if a typo is a runtime error and you're not "more productive" if you can't change a function signature without having to grep your codebase to find all callsites and pray you fixed them all.
Types are good, but as with anything else people take it too far. Making it your life goal to encode all business logic in the type system results in an even more incomprehensible mess than not having types at all. If the type name can't fit on one line in the error message, you've gone too far.
I've seen some insane Typescript type definitions. To be fair, they were defined to work with historic vanilla JS code where so many things could have been stuffed in the poor variable.
I'm forever grateful for Typescript, but I wouldn't be surprised to see some of that code in a future research paper titled "The era when types went too far".
The first time I saw this pattern play out was with CSS vs table layouts.
A lot of web devs these days don't remember a time before CSS but, back then, we used to use tables for page layout. This was really an abuse of the `table` element of HTML, which was designed to display tabular data, but it was basically the only way to achieve many layouts like having a side menu bar, for example.
Abusing `table` was bad for many reasons, including accessibility. People started to care about the semantic web and remembered that HTML is a markup language for content, not a style language for layout. So people advocated for using CSS for layout and reserving `table` for actual table data.
But gradually this rule became both simpler and more ridiculously followed. People started talking about "using divs instead of table". They would look at a page source and if they saw `div` it would get a nod, but if they saw `table` the developer would be relentlessly abused because `table` is old-fashioned. Eventually, of course, someone reinvented tables using divs and CSS.
And then people realised the pendulum had swung too far.
There's an irony that this comment is posted on HackerNews, where the entire page of comments is hosted in a big <table> for layout. Not sure if this is good or bad!
Agreed. I've left a few `any`s or `as unknown as X` in place before and typically leave a comment alongside it. Something to the effect of: "This code is bad and will take too long to refactor. Every time you trace a bug back to this code, please adjust the logic and typing a little bit to prevent the kind of bug you encountered. Eventually we can replace this bad code with good code."
I'm mostly fine with bad TS definitions since the types are cosmetic and you can always 'as unknown as whatever' your way out of an insane type definition. If someone nests 5 generics in Rust/C++ you'll have a much worse time.
If you're using type aliases or type inference you don't have to type out the long type, but when you get the error that you need `Foo<Bar<Baz<...<&Whatever>>>` but you have `Foo<Bar<Baz<...<&&Whatever>>`, you'll still have to spend 20 minutes dealing with it.
I think TS is a special case, since you can "just say no", but in languages with a statically typed runtime/no runtime I try to minimize the amount of <> in a type, even if they're hidden behind a type alias or inference.
You know, I‘d always been like “`from pdb import set_trace: set_trace()` and you can interactively edit. I don’t care what the type is because I can interact with my running program better than a compiler” (followed by intense nerd chortling).
You know, until the program is non-trivial: passing data between systems on a queue; using async, threading, and/or multiprocessing; compiled binary critical-performance libraries… and I’m left wishing I’d written the whole thing in Erlang.
If you have a tiny codebase maybe, but if there are a couple million lines, it's likely that a pretty significant number of functions are sharing names.
and then good luck catching that call when searching for say `asdf123`. This looks insane when I describe it like that, but you actually see it more often in JS than you'd imagine, and sometimes it may even look justifiable.
> if you can't change a function signature without having to grep your codebase to find all callsites
Every time I see a developer make this complaint about static typing I wonder what the hell they're doing with their type definitions and architecture where this is actually a problem.
> and pray you fixed them all.
If this isn't detected for you at compile/build time you're not using static typing
> You're not "moving faster" if a typo is a runtime error and you're not "more productive" if you can't change a function signature without having to grep your codebase to find all callsites and pray you fixed them all.
I don't see how either of these things relate to type discussions. A typo can certainly lead to incorrect code in the most strongly typed, staticest language there is (otherwise, what the hell is writing code even for?) What's worse: a runtime error or no error that produces the wrong result? Running a Python project might be quicker than compiling a C++ project. Dynamically typed languages can do better than grepping for function calls.
> A typo can certainly lead to incorrect code in the most strongly typed, staticest language there is
Yes but the >99% of possible typos (and nearly 100% of typos in identifier names) will be caught by the compiler. That's good enough for me to say "static typing helps with typos".
> What's worse: a runtime error or no error that produces the wrong result
From most bad to least bad:
1. No error, wrong behavior
2. Runtime error
3. Compile error
4. Red squiggly line telling you "this is bad" before you even compile
> Running a Python project might be quicker than compiling a C++ project
Yes, C++ is an insane language. You can have large C++ projects that compile fast, but you have to spend a non-zero amount of engineering effort. Generally "avoid templates" and "write the code in .cpp files, not headers" will ensure that you won't have hour-long compiles.
This is a problem with C++, not with type-checking in general.
> Dynamically typed languages can do better than grepping for function calls.
I'd love to hear how, I was doing quite a bit of JS at my previous job (not TypeScript, couldn't convince them :) and I never found a more reliable way to find callsites/variable usages, than grepping for the function name. And even that is not 100% guarentee, since you can call a function in some pretty insane ways,
anObject[`asdf${some string}`]
and then good luck finding this callsite when searching for anObject.asdf123
Add microservices and typos are still runtime errors, except now on another, except inside a container on another computer.
You still have to grep for callsites, although the scope is smaller. A much better way to reduce the scope is with modules - if a function is module-internal, you only have to grep that module for callsites. Better than having to scan the entire codebase, worse than not having to do it at all.
Performance with microservices + dynamic language is unjustifiably terrible if you're the one who's paying for the servers. You're already eating a 100x slowdown for using a scripting language, add microservices and you're eating easily another 100x for replacing regular function calls with network packets. I'm not willing to run a k8s cluster for a program my laptop could handle had I used the hardware better.
That's all assuming microservices are even an option, which is a very tiny percentage of all software.
> There are advantages to not using types, such as a faster development speed
In my experience, not even that. Static typing speeds up my day to day programming.
You touch this point on later with how IDEs can make your experience nicer with static typing, but I see this even with a REPL: statically detected type errors lead to meaningful error message that are much closer to the actual root cause, and let me fix it quicker than a runtime error would have.
It also liberates my brain from having to think so carefully about types. The compiler is disciplined so I don’t have to be. I can move quicker, with the confidence that a huge class of errors will be caught right away.
Static type systems are in my experience easier to use, speed up development, and increase reliability. The costs? So far I’ve seen only two: they may be harder to learn, and they’re harder to implement.
>> There are advantages to not using types, such as a faster development speed
> In my experience, not even that. Static typing speeds up my day to day programming.
Completely agreed. It also completely blows past the whole maintainability argument -- the junior developer you hire in 6 months is going to be much slower getting up to speed on your untyped code (using the "royal you" here).
For some, I'm willing to grant that it may be "faster" to write initially, but every developer reading your code written without types thereafter is going to be slower.
Some would argue that exactly that thinking is what makes the software cleaner and better- rather than mush-mash that passes the compiler. I.e what I’m saying is that it might be good to be thinking about exactly what you’re passing in, out and why.
Of course it's good to think about what you're passing, and why! Nobody disagrees with that.
The issue is, can you think carefully enough, consistently enough, so that you don't need static types? Can your coworkers - all of them? What about future you, and your future coworkers? What about that time that you're in a hurry, or going on vacation the next day, and you're not at the top of your game?
If computers have taught us anything, it's that automatic processes are more reliable than manual ones.
Personally, I just inherited a code base that is a decade old. I haven't measured, but I'm pretty sure that it's more than 100,000 lines. My current coworkers have two weeks longer on the code base than I do. Static types make this a lot easier.
And, if your approach to static typing is "mush-mash that passes the compiler", no, static typing may not save you. But dynamic typing wouldn't, either, with that approach.
Can’t say I agree. Having spent about 50/50 of my career doing weak/strong typing Im more and more convinced it’s very rarely I miss static type-checks. Though I have been blessed with skilled coworkers and sane languages.
The author is basically wrong on all the points.
I used to think like that for decades, but in recent years changed my mind completely.
Types lead to less bugs - nope, maybe marginally, but not significantly (see research on that)
Types lead to a better development experience - nope, I have all the definitions and vars in my REPL and so has my IDE. I get completion on all symbols, I can look at call trees, usages, refactor things with confidence, run functions in isolation, replace them, wrap them, all from the REPL and inside my application.
We encode everything in the type system - you can't. You need to do runtime validation.
And good luck untangling your type definitions when your requirements change.
That one is the killer. Static types are premature concretions on the ___domain data model as you understand them now. They will change and if you are unlucky you will have to support multiple different variations of ___domain models in the same runtime. Good luck with that. Especially if you use inheritance.
Granted, static types give compilers great leverage to optimize code, but, wouldn't you believe: there are dynamically typed languages that have static types as a la card add-on.
But for many, many use-cases, especially enterprise development, a dynamically typed, immutability-first, functional language pays off dividends in the long run.
The categorical error that is made by many strong typing fans is that they assume you would write the same code - just without types.
Well, you don't.
It's an inevitably frustrating debate because everyone has just drawn totally different conclusions from their experiences. For pretty much all your points, my conclusions are the exact opposite. (Except for "You need to do runtime validation" - of course that's true; but you absolutely can avoid doing most runtime validation.)
Just to take one somewhat at random:
> And good luck untangling your type definitions when your requirements change.
I think it is exactly the opposite of the case that static types make it harder to adapt to changing requirements. In every dynamically typed system I've ever worked on, there have been important assumptions about data structures - sometimes checked dynamically in pre- or post- conditions, sometimes only checked in tests, and often simply not checked at all - scattered all around, such that changing requirements necessitated reasoning through the impact of the change on all these implicit assumptions. It was terrifying to make changes. Sure, easy to make a change and start up the application with the new code without a fuss. But to know whether the change broke some obscure codepath I hadn't considered or been aware of? Very difficult! I much prefer a static analysis pass saying, "hey, you changed this interface, did you know this codepath over here relied on that thing you changed?". Static type systems are not the only way to accomplish this, but in my view, they are a much less burdensome way to accomplish it than the level of dynamic validation and testing necessary to do so otherwise.
But again, it's just a frustrating debate, because clearly you have the exact opposite experience! So where does it get us? Nowhere really...
> I have all the definitions and vars in my REPL and so has my IDE. I get completion on all symbols, I can look at call trees, usages, refactor things with confidence, run functions in isolation, replace them, wrap them, all from the REPL and inside my application
This is great, but it only works if you are executing the code that you want to inspect/complete/refactor. In my experience, that just doesn't scale well.
> You need to do runtime validation.
Type systems don't eliminate this, sure, but when used correctly, they _drastically_ reduce it, which is extremely useful.
> And good luck untangling your type definitions when your requirements change.
That's the best part - the compiler tells me exactly what I have to fix to get things working again. If I do the same thing in a dynamic language, I have to track things down myself, wait for unit tests to fail (hopefully I have 100% coverage...), and pray that there isn't an escape.
> And good luck untangling your type definitions when your requirements change. That one is the killer. Static types are premature concretions on the ___domain data model as you understand them now. They will change and if you are unlucky you will have to support multiple different variations of ___domain models in the same runtime. Good luck with that. Especially if you use inheritance.
I actually found this way easier with static types because with static types I could be significantly more confident about every single place one of those types was used, and look at it and see if it needed changes. This was way fiddlier to do in a dynamically typed environment
As a developer who's written hundreds of thousands of lines of C++, Python, and JS - I don't know! It's not so clear cut. I'm productive in all off them, although Python mostly wins. I wouldn't write a game engine or a video codec with it though.
JavaScript is inconsistent and weird, but Netscape's legacy long since stuck us all into it.
I can see in a very OOP-heavy style maybe, with huge nested classes, that compile/parse time static typing saves a lot of mistakes - but I've come to see OOP as mostly a disaster, and simple functions and structured data almost always win the simplicity and maintainability battle. And modern language servers / IDEs can catch a lot of typing errors during JS / Python development.
I'm persnickety about a lot of programming stuff, but never had a strong opinion about static vs dynamic typing. Both have millions of successful projects under their belt.
To be fair, C++ is a pretty rough language to be productive in generally, type system aside. GC is the single largest productivity enhancement a language can offer. I'd be way more interested in how Golang, with it's much simpler syntax/types (but they're still static) would compare. Even Java would be better, as, even though it's very verbose, the cognitive load for most tasks is a fraction of C++'s.
Java was a huge boost, by eliminating the cognitive load of manual memory management.
Scala can be used as a "better Java" with less verbosity, but also adds a number of extremely useful features; Java has slowly added some of them into the language over the last 8 years.
Have you tried Kotlin? I personally like it a lot as an alternative to Java and Scala. However, I hate any tooling around the JVM based languages. I cannot waste my time messing around with Maven, Gradle, etc.
No, I am very happy with Scala, and intend to use it for many years to come.
JVM tooling IMO is fantastic, it is one of the largest open source communities, and there are many options if you don't like Gradle or Maven. Also, as I understand it, the dependency management systems found in languages like Python and Node are horrible in comparison to the JVM, where it is usually not a problem.
It's really hard, even with Go there are so many more differences than just types.
e.g. in Python/Ruby I had a REPL and used it a lot. Go doesn't really have a good REPL, so I tend to not use it so much. I probably pay some in productivity for that?
And write and let gofmt sort it out. You can kind of do that in Ruby in theory (not sure if good formatters exist), but Python is more tricky (and I also think the auto-formatters for Python are bad and often make code worse).
The flexibility of things like Active{Record,Model} in Rails are great, and hard to emulate with Go. People have tried, and it just doesn't fit with the language very well.
Go's simple "just compile to a statically linked binary" distribution model probably saves me time, especially for distributing it to other people.
Probably other differences I can't think of right now.
So between all of that, how do you split out the productivity of "types" vs. anything else?
That's also my problem with the research on this, because you just can't, not really. What you need to do is compare e.g. Python with Mypy vs. without. I don't think anyone has done that(?) And even then you run in to the problems with differences in tasks, differences in programmer experience and ability, etc. which matter because for a lot of these studies n tends to be small.
In python I mostly code in functional style. Even then types helps me a lot. I make a lot of typos or get the order of arguments wrong. Typing helps me a lot here especially when I'm doing ML. The last thing I want is my training code to crash after it spent 30nmins processing the data. I find pythons gradual typing a really good middle ground for quick prototyping and the type annotate functions once they are mature enough
There's a simple fix for that, just do keyword argument only everywhere you can.
For me the hill I'm willing to die on is that labeled/named/keyword arguments are absolutely necessary and most of the usefulness of types is in fact just a shitty version of labeled arguments (god help you if you have a function where two or more arguments have the same type!)
The small downside to using kwargs is just the extra letters needed to call the function but I can live with that.
As for args with the same type, you can use phantom types I guess but I haven't explored it much in python. I'm pretty interested in the dfdx library in rust which can type enforce matrix or vector operations on it's shape. Stuff like that would really help me
The discussion over whether strong typing is better than weak typing
is settled [1], but not over whether static typing is better than dynamic
typing. Adherents of static typing thinks that the compiler should
check "correctness" by verifying type invariants, adherents of dynamic
typing think that is a waste of time.
I'm firmly in the latter camp. Because the compiler can't check
program correctness only type correctness. Type correctnes is
necessary for program correctness, but NOT sufficient. Static typing
adherents fail to recognize this, thus falsely believing that static
typing guarantees them more than what it actually does (blub).
Consider the article's birthdayGreeting example. Author is happy that
static typing catcges the birthdayGreeting("John", "20") bug because
"20" is not a number. But birthdayGreeting(" ", 123) is not
caught (" " is not a name) and neither is birthdayGreeting("Anna,"
-12335). birthdayGreeting("Anna" 4.5), though, is caught, which arguably
is wrong since 4.5 is an age.
This is actually very important since "type bugs" are trivially easy
to catch, while "semantic bugs" can stay undetected for years. An
account balance stored as a uint that overflows, a number that must be
prime at a certain program ___location, but isn't, a list that must not
be empty, etc. No, not even dependent types can guarantee these
invariants.
If you don't believe me, read up on some catastrophic bugs. Those that
have caused space ships to explode and cars to crash. To the best of
my knowledge, not a single one has been caused by bona fide type
errors. In the overwhelming majority of cases, the root cause has been
a semantic error.
[1] - Most people don't understand that typing is measured over at least
two axes, strong/weak and static/dynamic, and continue to conflate
weak typing with dynamic typing. C is statically typed and weakly
typed. Python is strongly typed and dynamically typed. Javascript is
weakly typed and dynamically typed.
Many of your examples could definitely be caught by static types, depending on the type system. But that’s less important than…
> This is actually very important since "type bugs" are trivially easy to catch
This, IMO, is exactly why I’m in the static type camp. It’s so trivial that you can do it declaratively, inline with the code it’s testing, with instant feedback, at every call site and in every downstream expression/statement. A type annotation doesn’t mean my semantic/___domain logic is sound, I still have to test that. but it could replace dozens of trivial tests orthogonal to the logic I care about… tests which, let’s face it, few people are going to write exhaustively otherwise.
The amount of runtime checks and/or tests required to emulate the guarantees of static types is huge. It's perplexing that some people would rather do that manually.
But to the point of triviality, static types tend to lull developers into a false sense of security. There are limits to their usefulness. You still need both runtime checks and test-time assertions for your logic. You just need less of them and they provide more value per line of code. But I've never seen a type system capable of expressing the entire problem ___domain in an ergonomic way. Not even close.
For my money, a combination of: static types, property-based testing, unit/integration tests, and an end-to-end test against prod are all required. You can compensate for the lack of one by putting more effort into another but you're really just shuffling the "correctness burden" from one place to another. I like static types because it makes the rest of testing easier. But at some point, you've got to roll up your sleeves and make sure it works IRL.
I question this "false sense of security" point. In my experience, people who prefer as much static analysis as possible, mostly based on types, are also more likely to be thinking about invariants and error cases more generally. I would go further to say that anyone who has really adopted a thinking-about-invariants-and-error-cases style of programming has almost certainly become frustrated with the amount of repetitive validation code required to accomplish that in projects with no static analysis of types.
Agreed. I was more rebutting the "If it compiles, it works" mentality - which is not true for anything other than trivial examples. You still need to test that the compiled artifact actually works in a real setting, where "works" is ___domain-specific and defined by user's perception of value.
Right. But I'm saying that I don't think that mentality actually exists. I think the mentality of people who prefer static analysis is more like "if it compiles, it has passed a huge number of test cases that I'm really glad I didn't have to write myself". But "if it compiles, it works" is a more pithy short-hand for that :)
In a dynamically language you need 100% test coverage, because misspelling a variable name turns into a runtime error.
On large projects this can be a problem. I worked on a Python server some years ago, and with every large program that supports exceptions, there's a catch block somewhere that silently eats the exceptions it doesn't process. Well, some modules were loaded dynamically, so my first spelling mistake (and second and third...) took quite a while to debug because things just didn't work, the code didn't get executed, but there were no errors. This also ate syntax errors, too, since that is also an exception. So if you edited the file, you had no idea if things actually still worked (or even compiled!) until you tested the functionality from the file.
> Consider the article's birthdayGreeting example. Author is happy that static typing catcges the birthdayGreeting("John", "20") bug because "20" is not a number. But birthdayGreeting(" ", 123) is not caught (" " is not a name) and neither is birthdayGreeting("Anna," -12335). birthdayGreeting("Anna" 4.5), though, is caught, which arguably is wrong since 4.5 is an age.
Author here. I think your examples illustrate my point rather than provide a counter example.
I mentioned later in the post that we validate when creating the type (from e.g. user input) so we know that the `Name` type is always valid (and " ") is not a name. So the type ensures that we have a valid name. So this will definitely be caught in our codebase.
As for birthdayGreeting("Anna" 4.5) and birthdayGreeting("Anna," -12335), number means a float in JS so it actually would be valid, though admittedly I had full integers in mind when writing this. Another example of where stricter typing than what TS providers (which Rust does!) would have helped better define the invariant.
So in short: the problem in the code there was that I was trying to show a simple example, which means I didn't define all the types as strictly as I normally would have, and it surfaced more bugs that would have been caught with types.
The specific examples you chose to illustrate this point are interesting, because to me, it is clear that what you want is a `Name` type that always represents a valid name and an `Age` type that always represents a valid age. Then you can put the validation in a single place (the constructors of those types) and write many methods like `birthdayGreeting` that can happily use values of those types without needing to take on the responsibility of validating them.
I honestly don't know of a good way to implement this pattern without type checking or at least optional type hints and static analysis. Instead, you can do validation of the input values in every single method, which is hugely burdensome, or you can assume callers are passing you valid values and write tests to make sure it isn't too catastrophic if they don't. I don't find either of these solutions satisfying.
It's funny you should mention this, as it immediately brings [1] to my mind. This well-known incident was due to type-checking, with acceptable ranges defined ahead of time for some types (which resonates with your comment: make birthdayGreeting accept ranges 1-150 or something, easy to do in ADA).
There are a few more well-publicized issues with spacecraft that could have been caught with better type-checking, including metric/imperial conversions (why not build the unit into a type? Though the fault probably was with integration testing for [2]).
Of course, you are right that type checking can't find every code issue (especially algorithmic), it doesn't replace tests. Instant feedback and type hints are invaluable during the development phase, though.
In your example, one could probably replace the first name string by a person type/object, by the way.
Neither of those were "bona fide type errors". The fact that both
errors ocurred while using statically typed languages shows
that. The orbiter failed because inches were interpreted as
centimeters. But as both datums were stored as floating point numbers,
which they always would be, in any reasonable implementation, static
typing wouldn't have caught anything. The Ariane failure was due to a
cast of a double to a short resuling in an overflow. Again, not
something static typing can catch because it can't statically check
the value of an arbitrary double.
No, even more types is not the solution. It's not pratical to wrap
every measurement in a Meter or Inch type (standardization is one
issue, is my Meter type the same as your Meter type?) and it doesn't
significantly increase the amount of invariants checked. You still
have range issues and you still won't get a statically check
PrimeNumber type.
I'm not familiar with ADA, but VHDL has similar range constraints
which are runtime checked.
> But as both datums were stored as floating point numbers, which they always would be, in any reasonable implementation, static typing wouldn't have caught anything.
Types are not runtime representations, and any good nominal type system will allow you to avoid this sort of problem easily. For example, this Haskell code
newtype Inches = Inches Double deriving Num
newtype Centimeters = Centimeters Double deriving Num
a :: Centimeters
a = Centimeters 1.0
b :: Inches
b = Inches 1.0
fine = a + a
alsoFine = b + b
mistake = a + b
will give this type error
Couldn't match expected type ‘Centimeters’ with actual type ‘Inches’
In the second argument of ‘(+)’, namely ‘b’
In the expression: a + b
In an equation for ‘mistake’: mistake = a + b
That is why I wrote "in any reasonable implementation". Embedding
physical quantities in the type system is possible in many language
but is not practical. Hence why safety-critical software doesn't
contain declarations like yours. For example, what is the type of Inch
* Inch? What is the type of Inch / Inch? Sure, perhaps if NASA and
their subcontractors had used a common well-developed physical quantities library
then the bug could have been caught during compile-time. But I think
it is a moot point since most people don't use such libraries.
Very true, it is possible and practical, and chrono is a good example. But you can take a similar approach in dynamically typed languages as well. It wouldn't be hard to design a library that did something like this:
some_duration = minutes(3) + seconds(2)
Of course that doesn't change the fact that any function receiving a duration would only raise type errors at runtime. For instance, someone could try to pass the number of seconds as an integer rather than as a duration:
remind_me_after(182)
But any semantic testing would automatically catch type errors as well. remind_me_after would fail every single time assuming the implementation would be something like this:
I think this is the reason why studies don't show huge differences in defects between dynamically and statically typed languages. You have to do semantic tests anyway, and they would cover most* type errors as well.
What would it mean for code to be semantically correct while containing type errors? It would probably mean that the code is not in fact semantically correct because some semantic edge cases are not covered by the tests.
* Not all of them because semantic tests could theoretically succeed by accident for some specific values that have incorrect types.
> What would it mean for code to be semantically correct while containing type errors? It would probably mean that the code is not in fact semantically correct because some semantic edge cases are not covered by the tests.
It can also mean that the code has incorrect subprograms, but that the program as a whole is protected from them by some hard to analyze condition. As an extreme example, one of these two programs "works":
if goldbachConjectureCounterexampleExists() then print("Hello world!") else launchMissiles()
if goldbachConjectureCounterexampleDoesNotExist() then print("Hello world!") else launchMissiles()
but I don't know which and neither does the compiler. Rejecting them is a feature, not a bug.
These semantics make the type incompatible with the Num class and attempting to plug your type into, say, a run of the mill ode solver likely won't do what you want. So you have two options. 1) Rewrite all code to become "quantity aware". Multiplying two Matrix[Inch] results in a Matrix[Inch2] and so on. 2) Litter your code with floatToInch and inchToFloat conversions. I think you can see why both options suck? If you really can't, then perhaps try finding some safety-critical software that relies on "physical quantity types" in some form or another?
Absolutely. Littering a code base with centimeter-to-inch conversions or rewriting every numeric computation to become unit aware would for sure have caused more bugs than it would have prevented.
> The fact that both errors ocurred while using statically typed languages shows that
Obviously it doesn't, since you can make a statically typed language program where every variable is a string map and every function takes variable args, and have no help from your compiler at any time because every type is always the right type. Sounds stupid, but in fact this is what happened in the examples.
The examples are bona fide type errors, because even though a statically typed language is used, multiple different types of numbers got the same static type causing the issue. So the people here were offered the solution to their problem from their ___domain (two types of numbers), choose to use dynamic typing (make both a float even though they're different things), causing the compiler to have no way of statically checking their code, and that caused a bug. It's the opposite of a counter example.
It is in fact the absence of enough typing that caused these issues. How much more typing do you need :)
Ultimately bugs cannot be prevented by having a type system. We make mistakes regardless of static or dynamic types.
We can argue it's because of the lack of types, tests, oversight and so on. I'd argue nothing can force a programmer to design a perfect system.
>The examples are bona fide type errors
I'd disagree because those are logical errors. You can represent those as different types with a type system, but you could also not use a type system and convert those values properly. Whether we represent them as different types or not is an implementation choice.
Those can be considered "type errors" (or missing types? a type of primitive obsession?) in a typed language. But it's still an issue with the implementation's logic.
In other words: the seatbelt is not a deciding factor.
Ultimately deaths cannot be prevented by seatbelts. People will die regardless whether they wear a seatbelt or not.
We can argue its because of a lack of seatbelts, roll cages, airbags and so on. I'd argue nothing can force force a person to be perfectly safe while driving.
> The examples are bona fide safety issues
I'd disagree because those are driving errors. You can present those as cars missing safety features, but you could also not use those safety systems and just drive carefully. Whether we use safety systems or not is a design choice.
Those can be considered "safety issues" (or missing safety? a type of safety obsession?) in a safety design perspective. But it's still an issue with the driver's behavior.
bit of hyperbole for flavour:
If we could just ditch the heavy roll cage, we could go much faster!
I don't see how this adds to the discussion. Second time this week for me where people are connecting programing discussions with people dying.
But sure, let's pretend this is a real argument.
Typed languages are not at all like seatbelts. They aren't an easy safety feature you can use within a second and suddenly X% of bugs are prevented. No research shows that it prevents the bugs we actually care about, and they don't deal with runtime issues that actually kills people like the one discussed in this thread.
Type checking is a programming safety feature, and its trivial to argue in any ___domain that a safety feature is optional. Its always trivial to argue this because safety is never a functional requirement, so it is never guaranteed to be needed for anything, and its easy to point out that regardless of safety, mistakes will happen, and that safety does not prevent all mistakes. All of this is trivial, and can be applied to any safety measure, this is why I brought up seatbelts: to show that your argument depends strongly on the context of where the safety applies, and the sacrifices that have to be made to gain that safety.
Which brings me to my second point: safety is always a tradeoff. Saying things like "perfectly safe" is completely void of meaning since nothing is ever perfectly safe, basing an argument on the fact that we cannot be perfectly safe is saying we shouldn't care about seatbelts because we're all gonna die anyway. Safety is a tradeoff and you should consider the up and downsides when you talk about it, always. So on your point:
> They aren't an easy safety feature you can use within a second and suddenly X% of bugs are prevented
Actually thats exactly what they are, because they will stop you from compiling a program that would have bugs in a dynamically typed system. Do they prevent all bugs? No, nobody is saying that. Its also easy to find how, just by googling: https://stackoverflow.com/a/27791387
> No research shows that it prevents the bugs we actually care about
No research shows anything about software, there is no research on software, stop deflecting.
> they don't deal with runtime issues that actually kills people like the one discussed in this thread
This thread is full of people suggesting type-safe fixes that would've prevented this, I'm not sure how you're still arguing that typing wouldn't help here. Again, maybe you don't want to use it because its too much work or some other kind of tradeoff you don't want to make, fine. But if you keep suggesting that types wouldn't work here you simply don't understand types.
I've never said that. It's always about trade-offs, but simply adding a typed system would not have prevented the issue, as it was already built in a typed system.
My whole point is not pro or against typed languages. It's that we need to move beyond that.
> Actually thats exactly what they are, because they will stop you from compiling a program that would have bugs in a dynamically typed system.
It totally depends. It only stops you if you are diligent enough to write programs with a very good type structure and hopefully you don't have any logical mistakes. If you type everything with primitives like uint you can still have a compiling program that's incorrect.
> This thread is full of people suggesting type-safe fixes that would've prevented this
And I could suggest fixes without typed languages that would also have prevented this, like better testing. The point is that a type system does not inherently change anything. It's still up to the developer and the systems around to be good enough to prevent catastrophes from happening.
Using types is most definitely one way of solving the issue, if implemented correctly. But the exact same could be done with a dynamic language.
> But if you keep suggesting that types wouldn't work here you simply don't understand types
Sure... I don't understand types ;)
In any case, I never said types wouldn't work here. Read my comments again. I said types are not the determining factor. The fact that this happened in a typed language simply cannot be overstated. Using a typed language or not affects many variables but it's not what will ultimately decide if a critical piece of software will kill someone or not.
> My whole point is not pro or against typed languages. It's that we need to move beyond that.
Ok, once you know where to move to we can consider that as a good solution. Until then, lets just use types, its the best tool we have.
Restated: car has seatbelts, seatbelts were not used, people got injured, hence "seatbelts were not a deciding factor, we need to move beyond that". OK, sure, I can say the same of whatever you intend to move to, and we will have wasted a lot of air to no benefit.
>But as both datums were stored as floating point numbers, which they always would be, in any reasonable implementation
It doesn't strike me as inherently unreasonable to use fixed point. At any rate, there are languages with built-in unit systems, so you could declare something like float length = 4.5 <m> and get an error if you try to add it to something in a different unit without having to roll your own dimensional analysis framework.
Of course, fixed point would have been a reasonable choice too. But here is my point. Someone who likes testing could say "This bug would have been caught with better testing". Someone who likes static typing could say "This bug would have been caught if they had used an esoteric type system with support for physical quantities." ... Which no one uses. Thus, you have an argument between something real and realistic, and something imagined and unrealistic.
That you aren't familiar with something hardly makes it imaginary. That exact formulation of unit system is, I think, pretty uncommon, but there are widely used languages (vhdl) where uniting is more directly part of the type system and widely used uniting systems as libraries (boost) where it is not directly a language feature.
VHDL physical types are nothing more than annotated integers. Making them completely useless for general arithmetic. You could not use them for converting from say Celsius to Fahrenheiht. It's cute that you bring up Boost which contains everything and the kitchen sink as evidence of "widely used". Show me a 3d engine that uses physical units for distance calculations. No one does it because it is a horrible idea.
How about "This bug would have been caught if instead of storing the distance in a float, they stored it in a struct/class that also contained an enum defining the units". Just about any language could handle that.
> The orbiter failed because inches were interpreted as centimeters. But as both datums were stored as floating point numbers, which they always would be, in any reasonable implementation, static typing wouldn't have caught anything.
They would not always be stored as flat floats. You could have type aliases for both values (type alias inch = float, the same for cm) and have not a flat float but an inch or cm as a type. Or, even better, F# has type support for such values: https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
You didn't provide an argument for why static typing is not worthwhile, merely that it won't solve all of your problems -- thus, you concluded, dynamic typing is better.
Yet static typing does very much catch issues, so I really don't see where you're coming from with this.
If I'm being generous then maybe 5% of my developer time is spent chasing type errors. And then most of those "type" errors would be errors that no reasonable type system would have a chance of catching - like keys missing from associative arrays. I'm competent enough to not need the compiler to tell me banalities like that age is a number or name a string. The overhead of having to deal with types is many times larger than those 5%.
>And then most of those "type" errors would be errors that no reasonable type system would have a chance of catching - like keys missing from associative arrays.
what? in a language with ADTs and exhaustiveness checking, accessing a key from an associative array will give you back an Option type and the compiler will force you handle the code path where the key is missing.
unless I've misunderstood what you meant, this is a canonical showcase of what modern static type systems are good for.
and in your parent post:
>a list that must not be empty, etc. No, not even dependent types can guarantee these invariants.
So static type system are conservative. If variable v is supposed to
have type T and the compiler cannot deduce that it for sure
is then a type error is generated. Hence you need option types because
static type systems are not powerful enough to deduce whether a given
nullable variable actually can be null or not. In effect, the compiler
throws its hands in the air and says "Nope too hard, you do it for
me." So the programmer has to insert runtime checks in the form of
pattern matching clauses for option types even when it is trivial for
a human to prove that a reference won't ever be null. The compiler
admits defeat and instead forces the programmer to use runtime type
checking.
It's mostly the same with nominative NonEmpty types. Any value the
programmer has instantiated with one of NonEmpty's type constructors
have type NonEmpty - everything else don't. For sure, you could
create a NonEmpty class in Java too, but it would still be up to the
programmer to enforce the type's semantic during program execution. If
the variable L has type NonEmpty (list) then it's bloody obvious to a
programmer that the expression pop(push(push(L, x), y)) also has type
NonEmpty. To a compiler figuring that one out is mostly impossible.
> So static type system are conservative. If variable v is supposed to have type T and the compiler cannot deduce that it for sure is then a type error is generated. Hence you need option types because static type systems are not powerful enough to deduce whether a given nullable variable actually can be null or not.
Option types serve other purposes, like composability; tracing program flow can determine the points where a nullable type either (a) will always be null, or (b) will never be null, and many type checkers for type systems with nullable types (or, more generally, non-discriminated unions, nullable types are just one example.)
> I'm firmly in the latter camp. Because the compiler can't check program correctness only type correctness. Type correctnes is necessary for program correctness, but NOT sufficient. Static typing adherents fail to recognize this, thus falsely believing that static typing guarantees them more than what it actually does (blub).
Uhm. What? I'm a static typing proponent and I have never believed that the compiler will guarantee program correctness. Nobody believes that. They believe it largely eliminates a certain category of error, which it does. You're entitled to your opinion on dynamic vs static typing, but this particular point of yours is just a wild straw man
I don't understand how you go from "this tool only does 90% of the work I need it to" to "I'll do the whole thing by hand" instead of "I'm fine doing 10% by hand".
Because arguments like these don't make sense in the first place. "The compiler won't let me compile my code in a static language after changes" does not mean that the compiler in a statically typed language is a hindrance but rather that the same program in a dynamic language has the samestructure, but it's hidden and not checked. In other words, the compiler is a safety harness for those factors it can check - even if it cannot check for semanticerrors.
In principal, at least, you could encode all those constraints in the birthday example into the type system. In practice, no one does, partially because existing type systems aren't ergonomic enough to express those constraints for developers to want to use them.
If you program in a dynamic language, then you have no choice but to perceive all bugs as runtime bugs. Because every bug you witnessed was a runtime bug, so why would types have helped you?
When I program in a typed language, the compiler rejects programs I write all the time. Things like: putting code that can't be rolled-back in the middle of a transaction.
> I'm firmly in the latter camp. Because the compiler can't check program correctness only type correctness. Type correctnes is necessary for program correctness, but NOT sufficient. Static typing adherents fail to recognize this,
That's just not true. Absolutely no one claims that static typing guarantees correctness, because that's ridiculous and obviously nonsense.
There's been considerable convergence on this. Most languages now have some degree of type inference for statements now. Even C++ has "auto". That substantially reduced the amount of type boilerplate in code. Remember having to write out long iterator types in C++ "for" statements? We're past that.
As for function declarations and structure fields, that's where you need type information to read the code. Once a program gets beyond a few hundred lines or beyond one developer, some amount of annotation is essential.
The main objections come from Python and Javascript users, of course. Python retrofitted a very strange advisory typing system, and Javascript retrofitted TypeScript. Both are bolt-on type systems and are used in mixed typed/untyped environments. That is painful.
LISP also got a type system retrofit decades ago, with "flavors" and the Common LISP Object System, and that was ugly, too. The lesson here is that retrofitting a type system creates a mess.
I actually think Python's type system is pretty good, given the circumstances. It's got some nice features like Optional forcing checks for None values before use, and structural subtyping with typing.Protocol. Could it be better if Python were designed with types from the ground up? Yeah, of course. But given the requirements of integrating with all existing Python code and not breaking any existing code I think it does a decent job.
The bigger issue with Python and static typing is the ecosystem and conventions that a lot of developers use, made worse by a lot of these developers really being data scientists who are writing Python. *args/**kwargs are heavily abused by people too lazy to write proper method signatures. It's extremely common in Python to have methods pass around DataFrames or dictionaries as a grab-bag of stuff. Bonus points when methods add and remove columns/fields so you don't know what's in the data bag until you run the code (or read every line).
You can do this in almost any language, of course. Nothing stops you from writing a C# program with every type being `dynamic` or have all your Go methods accept `interface{}`. But Python, for a long time, actively encouraged this approach, and sadly there's still many beginner tutorials today that present things like "just write a method that takes *kwargs and you don't have to change the function signature!" as an advanced language feature for smart people instead of an awful footgun.*
Python and Typescript "type systems" were designed for gradual, non green-field typing. It is essential to incremental migration and totally understandable that it works this way.
> LISP also got a type system retrofit decades ago, with "flavors" and the Common LISP Object System, and that was ugly, too. The lesson here is that retrofitting a type system creates a mess.
Flavors and CLOS are not "type systems". They are "object systems" and there is nothing "ugly" about them. Flavors was introduced into a Lisp which did not have a type system. Later CLOS was added to a Lisp which already had a type system: Common Lisp.
TS' type system is insane in what it's capable of expressing. I've personally wrote a snake game using types alone (you see a board, snake and snacks inside your editor type popups).
Despite that, TS is still quite cumbersome to use due to its initial mission statements. I.e. `Object.keys(myObject)` doesn't return `keyof (typeof myObject)`, so if you'd do something like this:
`Object.keys(myObject).forEach(key => myObject[key])` it'll throw an error that key is not assignable to `myObject`. The reasons lie in design choices made in order to be able to gradually migrate an untyped JS codebase into a TS one. There's tons of issues like this, some of them are just completely absurd and make developers that try to achieve simple things throw their laptop out of the window :D
This is an old, old issue, and it's not due to migration but due to inheritance.
`<T>keys(o: T) => keyof T` is actually quite incorrect, because you can't guarentee that keys() doesn't return a value not in `keyof T`, a key could be from a subclass of T.
I think TypeScript is doing the right thing here in not claiming something that's very easily violated.
And it’s not for lack of trying. You might say the claim has been disproven.
That said,I personally like static types, but primarily for the documentation effect that is, maybe not entirely coincidentally, the only positive effect for which there actually is solid empirical evidence.
"Types lead to fewer bugs when making changes to long-lived codebases" is not up for much of a debate in my opinion. Preventing regressions is arguably a lot more important than writing a correct first pass, which is what you are evaluating when looking at non-evolving code.
Concretely-speaking, an example of a bug being preventd by a typing system: Removing a field from an object in a large vanilla JavaScript project is inherently a minefield that has caused many bugs in the past, whereas the same alteration in a full Typescript project can be made with confidence.
I'm arguing that the studies linked in that blog post are fundamentally flawed because they only take into account first-pass development, and fail to take long-term maintainability into consideration. And this is silly because that's where the main value of type systems reside.
> This paper presents an empirical study with 49 subjects that studies the impact of a static type system for the development of a parser over 27 hours working time.
Well... It is true of everything in that blog post.
But I am wiling to have my mind changed. I would be really interested in seeing a study that failed to find a benefit of type systems for refactoring tasks in large projects. The example I posted in my initial comment seems pretty open-and-shut to me, so I'm curious.
It... should be up for debate? I'm fine taking it as a given in projects I'm running. But I do expect people that have the ability to do so, to debate it. Preferably to study it and find concrete evidence.
What I meant to convey is that the matter is settled enough from my point of view that there needs to be a very strong case being made for me to be willing to engage in that conversation.
Thanks for pointing this out, I've amended the comment to clarify a bit.
Charity to the idea, when boots are on the ground to do the actual work is a terribly place for running that debate. :D Is why I'm in agreement enough for projects I'm working on. I am open to the idea that we find more evidence and/or change our minds for the next project, but "when in Rome" is a good reason to drop the debate and basically declaim what your driving idea is at the start.
"Types let me make changes to code I haven't taken the time to understand" is hardly the proof you seem to think it it's.
Removing a field in a data structure is such a big problem, even in typed systems, that the most ubiquitous serialization IDLs, and customer-facing API conventions, don't even allow it.
And I would contend that anyone claiming "I am capable of taking everything about the code I am tasked to change into consideration" is delusional when it comes to non-trivial projects, including single-dev endeavors.
As far as serialization IDLs and API conventions go, they have the very special constraint of having to provide both forward and backwards compatibility across multiple applications, making them a very poor universal example.
Is there anyone making that claim? Apart from straw-men?
As an example, I was once refactoring a Java system. The compiler was happy after about a day, the unit tests after three days.
So if you claim your type system is going to keep your refactoring safe, I'd say that I'm not the one being delusional.
And of course unit tests also tend to, in practice, catch the errors that are caught by types. If you check that a value is equal to 10 rather than 9, you've also implicitly checked that it is not of type PinkElephant.
Now I perfectly understand the feeling that people who are used to the safety-net that static typing appears to give have when encountering a code-base without types. I have the same feeling when encountering a code-base without reasonably comprehensive unit tests (that fail when I poke into the code-base).
The feeling is absolute terror.
I just contend, with reasonably good justification, that this feeling is not particularly justified in the case of static types, because the evidence is just not there.
I haven't checked what the evidentiary status is for unit tests. But then again, I don't make these sorts of categorical claims for unit tests. I just make claims for my experience with unit tests.
> Is there anyone making that claim? Apart from straw-men?
For every intent and purpose, the person I'm replying to is, with an equivalent level of straw-man-ness, which is why I phrased it this way.
> So if you claim your type system is going to keep your refactoring safe, I'd say that I'm not the one being delusional.
No, I'm claiming that there are entire categories of bugs that are protected this way, not that it provides universal safety.
> And of course unit tests also tend to, in practice, catch the errors that are caught by types. If you check that a value is equal to 10 rather than 9, you've also implicitly checked that it is not of type PinkElephant.
It provides that if and only if the code invoking the tested function is called with arguments using the same semantics as the ones exercised in the tests, which is impossible to enforce in a dynamic language. I'd even go so far as saying that it is axiomatically impossible for unit testing to be a truly reliable tool in such languages because of the lack of precondition enforcement.
> the person I'm replying to is [making that claim]
Er, no. I certainly wasn't, and the person you were replying to above that was making the opposite claim: that it's harder than that.
And no, the type system doesn't really help all that much, once you have unit tests. Because unlike the unit tests, which checks values (and obviously the types as well, incidentally) the type system only checks the types.
So, trivially, if you mix up addition and subtraction, your unit tests will rightfully balk, whereas your type system is going to be "works for me".
As I wrote elsewhere in this thread, in my personal experience the type system was happy during a large refactor long before the unit tests were. Which means that there was a lot of stuff that the type system missed.
And so if you feel your type system is going to make your refactors safe, well, good luck to you and the poor people who have to use your code.
To address the unit test stuff: I'm not saying that type systems replace unit tests. I'm saying that unit testing in dynamically typed languages is fundamentally deficient.
Or in other words: unit tests are made more reliable by type systems.
Unit tests can only check outputs for the inputs that are provided during testing. Leaving the input unbounded means that they can only provide guarantees for code that conforms to the tested subset of inputs.
I may be able to test that `foo(3) == 4` and foo("foo") == "bar", but since I can't guarantee that `foo({})`, or literally anything else, will never be called, there will always be holes in the safety net they provide. This is something type systems directly address.
> And so if you feel your type system is going to make your refactors safe, well, good luck to you and the poor people who have to use your code.
safer. It makes them safer, not automatically safe.
I often rename a field in a typed language, just as a way to ask the compiler which parts of the code are affected by or use that field then I continue my reading there.
Its like a grep, except it can, for example, differentiate between "name" (of user) and "name" (of project)
> "Types let me make changes to code I haven't taken the time to understand" is hardly the proof you seem to think it it's.
I don't think you even understood the point at all here. "Types let me make guided and exhaustive changes to code that's much easier to understand due to the types and compiler support"
I believe this would count as empirical evidence of very strong static typing reducing bugs.
I think this points to what many other comments have been getting at, which is that typing is not a binary yes/no question, but a large, multi-axis (static/dynamic, strong/weak) spectrum with lots difference between type systems, as well as big differences in how people apply those type systems to their problem. You could still work in a static, strongly typed language and represent everything as strings, converting back and forth as necessary, but that's essentially working in a dynamically typed language. You could also take advantage of the tools the type system gives you in order to reap the benefits, creating classes representing the legal values and asserting important invariants.
The fact that strong static types makes the code self-documenting is the reason I vastly prefer it.
It makes me so much more productive, by orders of magnitude. And I say this as someone who has worked extensively in several different languages which covers large parts of this spectrum, like C, C++, Java, Python, JavaScript, TCL and others.
It makes it much easier for me to reason about code I have not recently worked on, be it in the current project or in dependencies. It also means I can be way more focused on the problem at hand, since I'm not constantly having to go on tangent goose chases to figure out what exactly I can do with this object that some function returns.
Sure there's the good, fuzzy feeling when the compile doesn't error out. But that's secondary.
Statements like: "The fact that strong static types makes the code self-documenting is the reason I vastly prefer it."
is exactly the reason why you shouldn't be use static typing.
We have a developer here who doesn't document any of their code and thinks that a good thing. Whereas in the dynamic typing world, they would be forced to use the good practice of documenting their code.
Code is never self-documenting, there are research studies on it.
You misunderstand. I'm not advocating you shouldn't comment or document your code. Sure I can see you multiplied that gross weight by a factor of two, but why? That usually needs to be documented.
But the types provides for let's say code-level self-documentation.
I can look at a function definition and see it returns an IReadOnlyStream. Well if I'm familiar with IReadOnlyStream I already know what I can do with that. If not I can quickly find the declaration, and based on that I can get a good overview of what I can do. If not I have something explicit to search the documentation for.
With languages such as JavaScript I learn nothing by looking at the function definition, and almost always have to dig deeper. If it's an unfamiliar code-base, then often much, much deeper.
> You would be better off using dynamic typing and returning an Iterator.
The point is that most of the time I wouldn't know it's returning an Iterator, because it's poorly or not document. At least that is my experience.
Anyway, I was only speaking for myself. And my personal experience is exactly the opposite: I'm far less productive in dynamic languages when working on any non-trivial codebase, for the reasons I stated.
Yes but if you were Tabula Rasa you would ship out software features 3x faster with significantly less bugs using dynamic typing than if you used static typing.
The argument for static types is that it makes type bugs impossible because the types are static. There is no emotional attachment other than rage when faced with "but but it does not eliminate ALL the bugs!!!!!!".
Type bugs are so rare in regular code that it's significantly insignificant when measured during research studies. From a code correctness point of view, static typing is simply speaking useless.
You should have spent the time statically typing your code on writing more tests.
Smalltalk is memory safe and dynamically type-safe. That is, you cannot invoke an inappropriate operation. You can try to do that, but the system will refuse to do so.
It's trivial to store sensitive data in a collection, for example, and then index it with the wrong index when trying to access and export adjacent non-sensitive data.
That's assuming you're mixing sensitive data and insensitive data in one collection. Heartbleed was access of random memory, a much more serious exploit
Academic papers are often very disconnected from industry reality.
I’ve worked on reliability at a company everyone has heard of for software that billions use daily. So many bugs would have been blocked by having a type system at all (dynamic languages on backend or JS on web) or by having a less bad type system (changing ObjC to Swift and Java to Kt on mobile). Null pointers/references alone have created so many bugs for us. Bugs that could have been compiler errors.
Research on this topic is beyond impossible and should be completely disregarded. You cannot possibly attempt to control for variables sufficiently, and I don’t think even the most fervent supporters of type systems would argue that they are a dominant factor in code quality and correctness.
I have to wonder if the author has ever used a strong, dynamically typed functional language like Clojure or Elixir or if his dynamically typed language experience is limited to JavaScript, Python and very similar languages.
It’s very easy to make up one’s mind about something and then completely stop learning.
I have worked extensively in Elixir and it only made me feel more strongly that static typing is the way to go. The sheer mental overhead caused by the lack of typing in Elixir is the biggest productivity killer for me when working in that language, and refactoring is almost as much of a nightmare as it is in something like JS.
For me, strong and dynamic is the worst possible combination. I actually need to be precise with types, but the language gives me no help.
I think Elixir has the best combination of runtime and tooling and ecosystem out there, and it’s type system is the only thing that prevents me from using it for nearly everything.
In Python, variables are mutable and being a class-based OO language, there’s often a lot of state that isn’t immediately clear when inspecting nearby code.
By working with immutable structs and pattern matching, you can get many (but not all) of the same guarantees you would from static types.
Guy L. Steele: "Don't you think it is ironic that type theorists who want to talk about strongly typed languages talk to themselves with an untyped language"
I still bemoan the half-assed RBS type system in Ruby 3. I think Ruby actually had a cool chance to explore what inline type hints would look like in a very meta-programming-y environment. While RBS works, it's a hassle to keep updated, and I think adoption is just going to remain low forever sadly.
Sorbet [0] is close, just wish the syntax wasn't as verbose as it is.
Crystal Language [1] is even closer, I just wish it had better IDE support.
I, too, find tools hard to use after I intentionally break them.
It's unfortunate that Typescript even provides this casting hack, though it's necessary for gradual, optional typing. The very first thing I would do on such a project is spend the time to fix those broken types, because it will pay back in full in productivity after a short time.
if you abuse the type system enough, most languages will have weaker typing.
"the language" is not enough per se, you have to use the tools it gives you to gain some advantage: those projects were clearly actively avoiding strong typing, for whatever reason
I think this comment doesn't add to the discussion.
It's like saying you can kill yourself in any vehicle.
Might be true (or maybe not), but doesn't add to the discussion of vehicle safety.
One might equally say that the embracing of reflection in Java and C# pretty much solidifies the case against static typing for complicated projects.
I'd tend to think the important feature here is strong typing, not static typing, and indeed TypeScript, which is hampered by the anemic type system of its parent JavaScript, it not the best example of strong typing.
Ultimately, strong typing is a hill I'm willing to die on, too. But a lot of people making arguments on this topic are conflating it with static typing, which just isn't the same thing. In general, that gives me the impression you haven't done enough work in enough different languages to really have an informed opinion. The type systems of different languages work within the context of the language as a whole, with tools like REPLs, test frameworks, constraint checkers, static analyzers, and IDEs carrying a lot of the work that might be attributed to a type system. There are a lot of confounding factors here, and if you're only talking about a few languages and you are making mistakes like conflating static=strong and dynamic=weak, you don't have the breadth of experience to understand what the benefits and downsides of strong types are.
Though uncommon, strong dynamic type systems do exist. Scheme is probably the most popular example, although its type system isn't the strongest. This is the approach I'm taking with Fur[1], which attempts to have as strong a type system as possible while still being dynamic. Python is... stronger than many other popular languages (i.e. JavaScript), but still does a lot of coercion (particularly around truth-y/false-y values--duck typing is a bit of a grey area in that it's sort of arguably weak typing, but also arguably indicates a capable type system rather than a weak type system).
But perhaps more critically, weak static type systems also exist, such as C, which is the source of many of the world's most serious bugs. It's highly unwise to assume that static typing is strong typing.
> We have projects in java7 still which span thousands of LoC and juniors can just jump in using their IDE and make some sort of contribution.
This seems to be a main reason certain people like it. It gives a sense of productivity. However I think it is misguided. It’s a false sense of productivity. Can they commit? Maybe, yes. Will it be right? Maybe, and more likely with types to handhold. But does it mean they understand the data model and the ___domain of the codebase they’re working in? No. No typing forces you to know what your code is actually doing.
It takes longer to be productive, but you’ll be productive because you know what you’re doing. Not because the IDE held your hand.
Widget factories or scrum farms will no doubt like the “ease” of jumping into a codebase that has typing, but I’m not yet convinced it’s better or that much better for the experience developer. I need to think on it more. For certain though I’ve seen enough in my time to know that how quickly a junior can “just jump in and make some sort of contribution” is not a good measurement.
Your argument sounds like a language should make it hard to understand parts of a codebase without understanding the underlying parts as well.
I would argue this goes against the goal of abstracting problems to keep complexity manageable.
Of course abstractions can be misunderstood and misused, but is that an argument for not having them?
"Truly understanding what goes on" and "static typing" are in no way correlated, let alone causations.
You are making a false dichotomy.
It's perfectly possible to work in a dynamic codebase without understanding the ___domain, business, logic or big picture. And it's just as perfectly possible to build a statically typed codebase that forces you to understand the whole entirety before being able to make a change.
Now, whether it's actuallygood to enforce true understanding of the Whole, before being able to work on a subset, is another debate. One that, unsurprisingly, has long been proven to be false. It's why we consider modules, functions, boundaries, coupling, classes, microservices, layers, and so fort and so on.
Nope. A new dev will need a lot longer to understand a large python codebase than a java one.
In fact, if all the developers change, it's a virtual certainty that none will ever understand the python code, while the odds are about even for java.
(But then, the types aren't the only factor for that.)
Strong typing is what allowed me to be a developer. I was never a great developer but I was good enough to be worth my salary. I was not smart enough to maintain a complete mental model of a a weak-typed software but I was able to do fairly complex stuff with the safety net of strong typing. My software developer would have ended much sooner (or never have been) without strong types. I had not other choice but to live or die on that hill.
All kidding aside, obviously I don't. That shouldn't detract from my statement at all though, nobody measures anything about software. We're all in the dark. Thats actually reinforcing my beliefs: we are in the dark because software is unmeasurable because of the freedoms taken by programmers that fail to abstract or fail to be formalized.
Formalizing software, and then starting to measure changes rigourously, is the first step we have to take towards becoming a mature industry.
Formalization is the oposite of freedom. People think software is like art, where freedom of expression broadens horizons. This is no longer true, most software is not art, most software is buttons that make money when clicked.
I wish the languages I use would not only provide static strong typing, but also proper explicit support for units. Automated dimensional analysis would prevent many of the bugs I've written over the years, and there are so many times I've had to think carefully whether a variable is in seconds, milliseconds or microseconds to the point where I always define my own types of these. Or whether a throughput count is in Mbits/second or Bytes/second.
That’s why we like Python and Elixir but not Javascript which is weakly typed. :-)
Since the author is willing to die for it, they might want to know that they probably meant “dynamically typed”. Some dynamically typed languages are strong typed, too!
Author here. Please read the post, I actually said "Strong static typing" in the post itself (starting from the first paragraph), it just didn't look good in the title. :)
That’s an important distinction. Why not just use “Static Typing”. Yeah, the word “strong” sounds strong and cool, but since it’s something you’re betting your life on, it doesn’t hurt to be a bit more precise.
Yeah, but your first point is specifically a weakness of weak types, not dynamic types.
I don’t really see any arguments against dynamic typing in there.
Most strong, dynamically typed languages also have good developer tooling (elixir and dialyzer for example) and have built in ways of describing data structures.
There's a section about catching everything at compile time rather than runtime which is very much about. I think there's a lot of value at knowing when things go bad at compile time, rather than runtime.
Back in the early 2000s, when I was a wee new SDE at Microsoft, there was a shared whiteboard down the hall from my office on which someone had scrawled:
"STRONG TYPING IS SO 90s!"
That missive stayed on the board for over a year. I still think about it from time to time and remember that the tech world's pendulum is always in motion.
Today? I adore TypeScript and I'm pretty excited about the PEP 695 QOL improvements to type annotations in Python 3.12. Where types and tools are concerned, we're in a much better place today than when Nirvana was still the next big thing.
But I won't be surprised at all if in another decade I see "STRONG TYPING IS SO 20s" marked on some corporate whiteboard in Redmond. :-)
While I agree with most of the post, it's just the tip of the iceberg of the discussion. Stuff like interfaces, traits, generics, concepts, inference, duck-typing, implicit conversions, etc. all fuzzyfy the notion of "Strong Typing" in various ways, some more than others.
The real discussion is not about types vs no-types, it's about the role and place for each of these bending of the rule and their associated tradeoff in different contexts. Op even takes inference for granted towards the end of the post, but make no mistake, that's a form of weak(er) typing.
For example, writing template code in C++ without frequent use of type inference (aka. auto) can be an absolute nightmare (I suspect that extends to generics programming at large, but C++ is where I have the most experience here), and the legibility gains from making use of it for iterators is broadly acknowledged as being easily worth the obfuscation for scope-limited variables. But there's also a strong case being made to avoid its use entirely in most other contexts.
C++ templates are a nightmare, because it is also a compile-time macro system. The cryptic, giant error messages from forgetting a > were legendary. Maybe this has improved, I haven't used C++ in a long time.
Java by contrast has a greatly simplified generic system; Scala improves on that with robust type inference, making generics pretty trivial to write and use.
100% agreed on all points. But even in these languages, writing generics remains greatly facilitated by the use of type inference, which is all I'm saying.
I don't think the opinion is even controversial at this point. Even the hardcore JS developers I know have come around to using TypeScript.
If you can't at least admit to the benefits of static types in 2023 then you probably don't have experience building or maintaining large software systems or working with a team of 15+ people, many of whom you've only met over Slack.
Personally I think Strong Static typing is what's required. But with an option to do dynamic typing to avoid faff in certain parts. Strong typing on its own doesn't really solve the problem that the author is describing.
c# pretty much has that nailed. You can do dynamic typing if you want, but you're on your own if it fucks up. Moreover its obvious when you're doing dynamic silliness.
Python is strongly typed, the problem is that its also duck typed and everything is dynamic. Sure there is type hinting, but that's mostly optional and pretty much broken as its not really supported at run time. (unless I've missed something)
What I'd really like is something like perl's "use strict;" in python.
I think there are at least two ways to write safe and robust code, one being to rely on static types to provide automatic guarantees, and another being to structure the program in a way where tests reveal errors immediately. They are mutually incompatible, and so different from one another that few people can be experts at both, but I don't think one is objectively inferior to the other.
Both styles require code to be disciplined, and undisciplined code cannot be caught by compilers, linters or automatic tools, it has to be through code review (or experience and sheer willpower). It's easy to think of examples where a lack of static types would allow brittle code to be written, but it's important to question whether that brittle code may be considered unacceptable by someone experienced in working without static types, in the same way that code using a Dictionary<string, object> instead of an actual type would be considered unacceptable by a type-safety practitioner despite being (technically) type-safe.
For example, with static typing one would define many different complex types over the program, in order to have the compiler enforce the many different constraints (here the value can be null, but not there ; these two values must be provided together ; and so on). Things like structural typing or algebraic data types even allows the creation of unnamed types on the fly. When sum types are used, conditional operators will be used to deconstruct these in a type-safe way, resulting in many branches.
With dynamic typing, one would have only a small number of very different types, because having many types increases the risk of using the wrong one, and having similar types makes it harder to notice that the wrong one is being used. The types must be very simple, because deconstructing complex types requires having branches, and branches should be kept to a minimum because they are hard to cover properly with unit tests.
First off, I appreciate the edit. python is strong dynamic typing, for example.
Second, it’s a shame that the author doesn’t really speak to Haskell at all. Knowing that a variable is a `u16` is rarely useful alone, relying on the meta of a variable name, for example, to prevent me from adding `user_age` and `number_of_legs`; they both are unsigned 16-bit `int`s, so the compiler is cool with it. A better type system allows types to hold semantic information beyond the bit-width or archaic and usually unhelpful information for the computer and not the developer. Relics of a past where 8k or RAM was all you could use for your program.
Ironically, the advanced Haskell type system looks more like dynamic typing specifically because the type inference is so good. You can rely on the compiler (or interpreter) to tell you what the type should be, often times allowing for much more flexible types than you’d think possible.
Also, being able to derive classes like Show and Read is amazing.
I'm sure the author is aware of these - many of the code examples are in Rust which has newtypes and automatic deriving of traits (ex. Display which is the equivalent of Haskell Show)
I wish people wouldn't conflate dynamic typing with weak typing. Python, for example, is dynamically typed, but it's strongly typed too. You don't get bugs like `1 + "2" = "12"` in a strongly typed language like Python.
You can still get runtime bugs, of course, but that kind of thing would manifest as a TypeError or something. All of those bugs could be caught with tests.
I like dynamically typed languages. They are simply the pragmatic choice for the vast majority of code that will ever be written. But being dynamic doesn't mean you can't do types. Python has optional type hinting and checkers like mypy are very good. Common Lisp has features that enable you to declare types and get near-native speed binaries.
These days I do type my Python code for many of the reasons given in the article, but I still enjoy using a dynamically typed language underneath.
I agree with the title, but not with the blog post. Strong typing is great. There is little reason not to use strongly typed systems.
The post adds "static" typing in too, modifying the argument. Dynamically typed languages allow you to easily have a very intimate understanding of your program; because you can write the program and modify it while it's running using a REPL. The post seems to conflate dynamically typed with untyped, which is very much not the same thing. The two are actually incompatible ideas.
OP: I highly recommend you look into SICP if you want a concrete counter argument to static typing for large systems. A main focus of the course is controlling complexity through abstraction as systems grow to be large, and the course was taught using Scheme, which is a dynamically typed language.
I want different things at different times. When I'm first exploring a problem space or figuring out an approach or fooling around in a REPL, dynamic typing is great. In those circumstances, the ability to quickly test (and reject!) ideas is critical, and I don't usually find static typing worth the friction. On the flip side, when I'm working on something longer lived or mission critical, it's important to set the maintenance brigade up for success. That means, among other things, useful doc strings; detailed and informative design notes; and type declarations with the right level of specificity.
Of course, the coin has a thin edge where the situation is a bit of heads and a bit of tails. Count me a fan of gradual typing.
I always have trouble with takes like this because they are context-free. There are a wide variety of project types and development scenarios.
My nuanced take is that typing is an economic choice. If the cost of failure (MTTR and criticality) are low enough it is fine to use dynamic typing. In fact, keeping the cost of failure low (if you can) gives you much more benefit than typing provides.
Erlang, a dynamic language used to create outrageously resilient systems, is a great example of that for the domains where it can be used.
I'm not a dynamic typing zealot (I like static typing a lot) but I do think that dynamic typing is unfairly maligned.
The cost argument brings the decision down to earth.
I've been in the same position. Modern Java is a solid language. Most "enterprise" companies are still using Java 8 or if you're very lucky Java 12, so you really don't have access to all of the modern functionality and because of that I hated on Java for years. Java programming in the wild is a crap shoot at best.
You mean 11, right? 12 was not an LTS release, and has been out of support for years. The currently supported LTS releases are 8, 11, 17, and the recent 21 release.
Static typing is useful if you are building some program for other people to run that you want to be reasonably robust.
However, it does not work well with interactive programming. If I had to declare all the types every time I do something in Mathematica, I'd be severely annoyed for no benefit.
Common Lisp seems to be the best of both worlds, with dynamic strong typing in general and advanced compiler type inference that can correctly point out mismatched types in most cases in SBCL, leading to much easier time programming interactively.
If taken to an extreme then nearly all parameters can be made into unique types. Not just measures, but pixel height measures. Not just pixel height measures, but help panel pixel height measures. Not just help panel pixel height measures, but settings help panel pixel height measures and so on. This may seem like a great way to keep everything well sorted, but ends up being an unhelpful mess that is difficult to maintain. So the question is how exactly in a situation is best to define types, not whether or not to have them or to make them static or whatever else.
And why exactly do you need types in the first place? Lots of projects are messy and involve teams and have work handed off to developers on a regular basis. In that kind of context which is quite common types can be a life saver. There are also short term one person projects that have a high expectation of all work being thrown away soon after. Isn't defining a bunch of types likely to be a waste of time if there is only a brief development effort which is clear and not expected to ever be touched by teams?
Most one size all solutions like static typing have some contexts in which they are either a complete waste of time or need their breadth to be carefully limited.
It's interesting we've gotten to this place where the discussion happens like it is on this forum, where acceptance of strong static typing is pretty high.
10 years ago the bias was strongly the other way. And in many shops I've worked in, there was an intense hatred of having to declare types.
I like to think it's the development of better/smarter type systems in mainstream programming languages (vs say Java which is what was dominant then), as well as maybe maturing of software engineer practices.
But I also fear it's just the trend-pendulum ticking.
That and I notice a lot of Python jobs out there, which is an ecosystem I've kept away from for the last 10 years precisely because my professional experiences with large dynamic-typed late-bound Python codebases was terrifying. Beyond NumPy & ML related projects, I fear that this might be indicative that the "types are just getting in my way" crowd is still very influential...
Finally I also think it's important to distinguish two concept that get mixed up -- type vs binding: static typing vs dynamic typing and early-binding vs late-binding. It so happens that most languages that emphasize static typing emphasize early-binding, and vice-versa; but it's entirely possible to have very strong types but still introduce sloppy late binding decision behaviour that ends in all sorts of runtime exceptions. Java is/was notorious for this. Reading configuration files to drive application behaviour, or doing things like the Rust Axum framework's "axum::extract::State" stuff, which can blow up at runtime; they're not strictly type issues, but get mixed up in some of the things people are talking about here.
"Move-fast & break things." "Test-first-design & aggressive unit testing is the solution."
To be charitable, perhaps that the type system was getting in the way, physically of what they wanted to accomplish conceptually.
I actually think it has more to do with object-oriented programming as it was propagated through C++ and Java. We were told (and believed) that OO abstractions would solve many software complexity problems. The focus of a lot of programming work was/is on bringing these OO design patterns into play. In large part the OO thing is about a kind of dynamic dispatch. And yet languages like Java imposed a very rigid way of doing OO, and made some of the patterns awkward to express.
So if you believe in your heart in the promise of OO, but find that your OO experience is relatively handicapped by your static type system, one then blames the type system. Ruby was maybe the best example of where this line of thought was going/coming-from?
I think we are getting past this by getting (somewhat) past OO as a dominant design methodology. But also by adopting languages with richer static type systems.
This debate is a lot older than typescript vs. javascript and dates back to at least the nineties when languages such as python, perl, and others were creeping into production systems. Before that there were already things like tcl/tk and a few other things of course. And of course people were messing around with things like Basic as well.
A lot of the arguments against typing are a bit stale.
- We have transpilers now. Even most javascript coders use a transpiler or at least a minifier. The overhead of a type checker in such a tool chain is pretty minimal. Especially on a modern laptop.
- We have type inference and a few other modern compiler features now. Typing is a lot less verbose in e.g. Kotlin than it is in Java. Java now has a little bit of type inference but it is still pretty verbose in comparison. A lot of the typing is just added by tools as well.
- A lot of languages like python, ruby, and others have types now. And spin off languages that have better typing (e.g. Mojo, Crystal) and preserve most of the original languages perceived elegance. Just goes to show that typing can be done without too much compromises.
Some early Lisps had both compiled and interpreted mode, where the interpreter used dynamic scoping and the compiler used lexical scoping. (Which, I presume, caused much developer confusion when code worked fine in the interpreter and got very strange behavior once compiled.) Something analogous (but more sane) could be done with type systems.
I'm very much a fan of strong static typing, but I understand the appeal of dynamic typing, particularly for small scripts and/or prototypes.
Static type checkers (for most languages) are pretty simple theorem provers. For statically typed languages, the compiler (or, I suppose in rare cases, interpreter) rejects any program that it can't prove is free of type errors. An alternative would be to only reject programs that provably contain type errors. The difference in allowed programs covers the cases where the type checker can't prove either way.
So, I'd love a language with a type checker that when ahead-of-time compiling code would refuse to emit libraries/binaries when it couldn't statically prove types were used correctly. In interpreted mode, it would only refuse to load code that it could statically prove contained type errors. Maybe when used within an interactive session, it would still allow (but warn) when code provably has type errors.
As an added bonus, static type guarantees open up more opportunities for the optimizer. A single language offering some range of static guarantees would reduce the number of cases where dynamically typed code gets re-written into a higher performance statically typed language when re-factored into libraries.
If you prefer, go ahead and dynamically type your scripts, etc. However, if/when it comes time to package up parts of your program for wider re-use as libraries, please tighten up the static guarantees of type safety.
>So, I'd love a language with a type checker that when ahead-of-time compiling code would refuse to emit libraries/binaries when it couldn't statically prove types were used correctly. In interpreted mode, it would only refuse to load code that it could statically prove contained type errors. Maybe when used within an interactive session, it would still allow (but warn) when code provably has type errors.
There are so many benefits for a front end application (written in javascript) to just assume it will be the backend that does type validation. A new field column added to a table? Cool, update the api layer, but the javascript just gets "the object" and now you have access to it immediately. No need to duplicate your definitions in both code bases, and it's not like front end security is real anyways. Sounds to me like a lot of "I learned to code in some derivative of C" programmers couldn't possibly consider that different languages could be optimized for different use cases and just want all coding to be "what they're used to." Then they moralize it and use shaming language to push it on everyone else. F typescript. I use different languages for different things, and strong typing makes a ton of sense IN MOST INSTANCES, but certainly not all.
People are conflating too many ideas. You're right that there's a huge benefit to that, but it's contigent on you as the dev being able to trust the types coming into your system, you need good docs, and attention to detail as you code.
All the TS devs just conflate JS with "no types" and picture drooling idiots that make random changes without knowing the type of anything (probably because that's what they do without 50 IDE popups to guide them). You're never going to convince them of the values of dynamic typing because they have a different mental model of it in their head.
Typing is literally an architectural/design decision. There is no such thing as one single type system that works for all use cases.
It's like saying there's only one good kind of metal fastener for construction. Sometimes you should use nails, sometimes screws, sometimes bolts. It's not like one of those is always superior to the others. You literally should not use a bolt all the time, nor a screw, nor a nail. There are specific engineering requirements and purposes to them all and you can't just mix and match based on your personal preferences.
Well, let me correct myself: you could just use one fastener you prefer, but your building would fall apart six different ways and be a huge pain in the ass to construct. Thankfully we have building codes so people can't just decide to do that.
I doubt anyone is arguing that our bash scripts should be typed, or our quick "remove_currency_from_bank_csv.py" must be statically typed.
The article puts a few cases forward. What the article is discussing, however, is the majority of software. Built in varying teams. Over long periods of time. Large software, complex software.
I just had a moment today. I'm developing a front-end thing in TypeScript. I'm messing around with dates across different data types (date string, Moment, a Neo4j-compatible date type, and Javascript's own Date type). Real fun, as you may understand. Struggling to convert a specific type straight to a js Date, I decide to just `new Date()` and add all the fields I need. `jsDate.setMonth(convert(otherDate.month))`. Red squiggly line appears underneath. I first realise it's a js Date, so I add a -1: `jsDate.setMonth(convert(otherDate.month)-1)`, and the red squiggly line disappear.
I feel a moment of horror. Type coercion. In front of my very eyes! I quickly added proper conversion everywhere.
I have no problem with dynamic types, but type coercion is the devil.
I like the idea, feels very pythonic ironically. And I suppose if you don't want the generic, I imagine there is probably some subclass of that which is more restrictive.
Anyway, as a developer I love when people do this. As a developer this sounds like a nightmare to maintain. My coworkers have told me to limit the numbers of 'what ifs' and just roll with it.
You can think of generics a meta-language that restricts and documents some behavior one level above the concrete types that you might need..
Lots of benefits, code re-use, documentation as code, less bugs..
Granted it's not always easy and straight forward but that what senior developers in the team are for..
Clojure author Rich Hickey disagrees on this. He argues that not only typing is what you need since many of the errors are not typing errors and not even most are (not sure he said that last thing exactly though, talking from the top of my head). The point being that an alternative that can check that and more is better. Also, he argued that types are key/values no matter the formal type if it is the same thing in two systems.
So he created spec. Spec can check more invariants than just typing. Truth to be told, Clojure is functional and one of the goals is to generate test cases from those specs. So it is a bit of a different story probably.
Static typing won there's no point arguing over it anymore. The issue is the type systems. Someone writing Haskell will have a very different opinion of types versus someone writing Rust versus someone writing Typescript versus someone writing Bash.
That's why you see a negative reaction to static typing. Its the developer experience. Its the error messages. Its the amount of time and mental effort it takes to type something correctly.
If you told me I had to choose between dynamic Python or strict Typescript I would choose Python. I love static typing. I hate Typescript.
So don't die on a hill! Just make better type systems!
This is a whole vibe when working with an ML language. I usually start at the bottom of the call stack and add the property I need to the signature of that function, then I can just follow "hey, you're calling it wrong—where is argument xyz?" errors up to the top of the stack. Classic bop mode.
I wonder if most opposition (or even ambivalence) towards strong typing comes from never having used it. If your entire career has been JavaScript, Python, shell scripts, etc.; then you have zero experience with it.
I wonder if most opposition (or even ambivalence) towards strict typing comes from never having used it. If your entire career has been JavaScript, Python, shell scripts, etc.; then you have zero experience with it.
You probably mean static type checking. I've used strong typing in the first half of my career and have no hard feelings about it. So no, the opposition isn't about no experience with one or the other.
I taught myself Turbo Pascal in middle school (34 years ago) and C/C++ in high school. (Both typed).
Over the years, I have done programming with C, C++, Java, C#, PHP, Python, OCaml, Lua, Nim, Golang, Rust, Objective-C, Flash, Bash, D, Scala, TypeScript, assembly, PL/SQL, F#, and a few that I am forgetting.
Compile time type checking can definitely make things easier.
But for dynamic languages that I have been programming in for many years to have types shoehorned into them as an afterthought feels like a kludge.
And it's a tradeoff. I got used to developing in Node or Python with vim without auto completion (although I had those things as a kid in IDEs for other languages and they can be great). And without compile time type checks.
You can pass JSON around and literally never do a database migration.
This stuff will lead to more runtime errors initially, but also means more streamlined code. And you have to do thorough testing regardless.
TypeScript might be a choice for a large project with several developers. But I would also argue that an even better choice would be just to use a different language that has the typing system you want built in rather than awkwardly bolted on.
What I see in most TypeScript projects is a half-assed attempt that still results in run time type errors, but with the added "benefit" of an awkward bolted on typing system and the need for more tools as well as losing any dynamic benefit.
They drop the idea of using JSON and make everything related to the database compile time checked if possible. I'm not saying that those checks can't be useful but if are going that route it would make more sense to just drop the dynamic language.
Having all of the dynamism can be a convenience and save you time and effort in other ways. But not if you treat it as a poor man's static language and shoehorn in types in a half assed way using a traditional database.
Part of this is that people think software engineering is about your choice of stack or adding processes to development.
What matters most is probably the feedback loop between the users and the developers. Starting with the requirements engineering. Second to that would be details of the software design and organization. Leveraging existing code effectively can be key. Using descriptive but relatively concise identifiers, short functions, good code organization.
But diving into and evaluating all of that stuff in detail takes effort and a lot of skill and so many make snap judgements based on surface level processes or tool selection.
The problem with strong types is how strong is right?
If you have a truck with a 1000 liter tank on the back the type for the contents liter, or is it something liters_H2SO4 - if you only need to go to a gauge liter is good enough, but if you are doing anything else you do not want to mix the contents up with liters_H2O. Note that is is very likely you will want to have both in the same program in different areas! Of course we can take the tank out of the truck and replace it with a box of iron - so maybe we need an even more generic type that could be either liters or kg?
As someone who has programmed in C++, VB, Clojure, Java, Ruby, Erlang, and now Javascript (because of NodeJS and that it is in the browser), I will admit that I usually ending up implementing my own Type system and checks more and more as programs gets larger. Maybe there is some variation of Greenspun's tenth rule like "Any sufficiently complicated dynamically typed program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of INSERT YOUR FAVORITE TYPED LANGUAGE HERE"?
It's a shame so many people have had bad experiences with static typing due to Typescript. In languages like Rust and Elm, static typing truly feels like a super power when I'm coding, particularly when modeling my applications possible states.
This talk does a great job at articulating how to use types to disallow invalid/illegal states.
I did C/C++ for the first 15 years of my career. I generally agree that strong typing is better for everyone.
But when I started working with python, I understood the beauty in it. At first I couldn't believe you could have a system running reliably that was being hit by millions of people around the world, but then that's when I fully appreciated the power of automated testing. I came from an enterprise shrinkwrap environment with 1.5 hr dev cycles, so you can forgive me for not understanding CD/CI at that point.
Yes, you can't detect when changes could cause bugs, but that's where testing comes in. Where I worked, we had to have complete code coverage so that every line of code was tested, and we needed to have a very strong automated testing. Using that method of testing, it made it a lot more predictable.
Sure, bugs can leak through, but having worked in C/C++ for so long, I can assure you that static typing only fixes a certain number of bugs, there are plenty more sources of bugs that just types.
So for the record, I like static typing and generally agree with using it, but I also see the beauty and simplicity in dynamic typing like Python and it's not complete chaos and there are ways to mitigate it.
The example from the article without any explicit arguments reminds me very much of objective c's message passing concept, which I actually quite liked and used very much. So, I guess the answer is: there is no objectively correct way to do things, except if the thing you are trying to do is get "engagement" then take an extremely strong stance on something which has no objectively correct answer.
I also don't want to get into a debate about something where there's no chance of changing an opinion. An absolute position is like a religion - it's the desire for everything to be simple, for the removal of doubt and the need to think about each situation on its merits.
I remember taking a badly written Python project which was difficult to work on because it took hours to run and could fail at the very end due to a syntax error or other trivial kind of mistake. I needed some way to make it less difficult to change so I could change it to something good.
I tried applying type hints because I thought this had minor impact for potentially some gain. In this case it didn't and what helped was to take every tiny bit that was unit-testable and make a test and then to mock the part of the process that took all the time (it was an android build system so I mocked the android build). Every bit of refactoring-for-testability made it easier to do the next bit.
So to me I feel that writing tests has more value than any other activity - I'm ready to use types when it makes my IDE work better and when I think it stops me making a mistake I've made before but for my money tests deliver more and I will rather work on them than trying to build an elaborate type system that models the problem well enough to prevent one from doing invalid things under any circumstances. For me, the types/classes I create are there to make my program understandable and manageable as it gets bigger.
I find it useful to be able to choose where to put my effort. When I worked on Java long ago I found the type system of the JDK libraries horrendously overdesigned and hard to use - flexibility is sometimes bad if you don't gain from it but are forced to pay the price anyhow.
Python is an example of adding types after the fact. I don't know the specifics of your situation, but gradually typed languages do not offer the same guarantees as languages that have static type checking from the beginning.
Replacing types with tests adds giant amounts of boiler-plate code, for little benefit.
My experience is that tests model the expected behaviour and that type systems cannot. Therefore I get lots of value out of tests where types are an impediment.
Static typing from the beginning just sets me into a big design up front problem where I try to anticipate the flexibility that I will need and I start building lots of boilerplate abstractions that end up never being used or restricting me later.
I find it useful to write imperfect code with only the constraint that it is testable and then revise the structure and any types I need as it becomes evident that they are valuable in the context.
Big yawn. I'm comparatively old and have programmed computers for a long, long time as one of those unfashionable "dark matter developers." What a lot of people see as unassailable truths I see as fads. Just as an example, I've never had to go wild about types. One day I will set aside some time to see if they're worth the overhead, but chances are I will not be a convert.
Only this toolchain will be used because it is the only one that makes sense. Everything will be so much easier if it we just did it in the functional style. Large programming projects can't exist without Agile (I've actually heard someone say this). J++! X Will Eat The World. No comments! Verbose comments! If we got the whole team on this one editor, imagine the synergy. Silverlight will take over the interactive web.
If types turn into a big deal and remain that way say, fifteen years from now, then it is probably a Good Idea But Not Strictly Necessary. Whatever it is people are so bound up about, it is likely that programming used to happen without it.
The only constants we have in programming are variables.
Does anyone here know how Standard ML inferred types? I really loved coding in SML and its strongly type checking system. The amount of bugs I produced was noticeably less. What I'm wondering is why we can't use a similar system as SML to infer the types. Other languages haven't adopted a similar system so there must be some kind of trade off. What was the downside?
> What I'm wondering is why we can't use a similar system as SML to infer the types.
We can and do! Use SML!
People just don't find out about the good type systems because the market is crowded out by mainstream shit.
And people write pieces about how good this shit is: "I was going to add an Int to a String but the compiler stopped me! Less [sic] bugs!". With arguments like that, why would you switch camps?
Or sometimes you get fed shit and get told it's inference: "I can replace the first word on the line with 'var'!".
So what are the downsides?
Inference doesn't play well with inheritance or mutability.
If you read Effective Java and see the points about avoiding mutation and inheritance, then these downsides may very well seem like upsides to you.
SML has never been used in any of the jobs I've had. I definitely have a very functional programming bend to my preferences. The closest I've come to doing something like SML for work is to use Clojure.
> Does anyone here know how Standard ML inferred types?
SML uses Hindley-Milner (with some modifications to support imperative code). Many statically typed functional languages descend from it: Haskell and OCaml are the most prominent.
> Other languages haven't adopted a similar system so there must be some kind of trade off. What was the downside?
If you're asking why existing popular languages don't adopt it: they can't. HM type inference requires a HM type system, and they've committed to a different one. At best you could get good inference on a HM-typed fragment within the language (though even that would mean making a lot of painful tradeoffs) but it would break down the moment it made contact with existing code.
If you're asking why HM languages didn't become more popular? Historical accident, in my opinion. I don't think it has much to do with the type system, given how fundamentally awful Java's is.
If you have strong static typing you don't need all those dumb functions like string_to_number("1234"), etc. to change the type you're given to the type you find you need right now.
When you know that a variable is of a set type, you DO NOT need to keep looking to see what type it happens to be at the current moment, and then juggling it to suit.
What do you call the Common Lisp sort of typing where you can drop hints wherever the machine is guessing at the type, and it will yell at you if you do something dumb with those things. But you don't have to drop hints at all and it will still compile. It is strong when you turn it on but dynamic otherwise. Strongly typed with inference, maybe?
I gave TypeScript a shot on more than one occasion, and it always ended in frustration after I inevitably encounter some obscure ts(42069) error that I have to research, then find out that a compiler bug is causing the error, and then get told "it's fine, just add @ts-ignore to the top of the file! See, your code works now!"
Your statement reminds me of the spoon scene in The Matrix: Instead of trying to bend the code to fit within the confines of its types simply try to realize the truth... Static types are just more code. They're extra conditions that we apply to things like function signatures and variable definitions instead of adding conditional checks where it matters.
"Aha!" you say, "but by enforcing conditional checks at variable definition and inside function signatures we don't have to check them ourselves in our code and the compiler itself can check them to ensure correctness and optimize performance!"
...and I'd agree with that sentiment but I'd also add that by having to reason about your types constantly you're adding a non-trivial amount of mental overhead when structuring your code. For some people that's how they reason about their code anyway but for others, having to consider whether or not a number is a `u8`, `u32`, or `usize` adds a lot of complexity to the code that could very well be entirely unnecessary depending on the use case.
Consider, for example a program that's meant to be executed on the command line that takes two numbers and adds them together. No matter what language you use you have to convert the raw string input into a numerical type before performing the addition.
In Python you have a simple function, `int()` that will happily turn any string consisting of numbers into a proper integer that can be used for math operations. If it gets something that isn't a number it'll throw an exception which is trivially easy to detect and wrap in a `try`/`except` block with a user-friendly error message. If we wanted to support floating point numbers we could just use `float()` and the code would otherwise be exactly the same.
In strongly-and-statically-typed languages this same process involves considerably more overhead. Before we can select a function to convert a string into a number we must first pick a numerical type and before we can do that we must think real hard about how big of a number we're going to support in this application. Our function signature(s) could also get complicated because, "wait: what type of string are we going to get from the command line?" Something as simple as adding two numbers becomes complicated really quickly!
Comparing the two ways of handling things (strongly, statically typed VS strongly, dynamically typed), Python ends up the clear winner a lot of the time because you got the job done without having to think as much, with less code that's super easy to reason about even though it didn't use static typing.
The other problem is that when you write such trivial programs in strongly, statically-typed languages like C, C++, Rust, etc it requires a lot more mental overhead to look at the code to figure out what's going on/how it works (though with Rust, less so because there's not 1001 awful ways to do things haha).
> In Python you have a simple function, `int()` that will happily turn any string consisting of numbers into a proper integer that can be used for math operations.
For starters this is not the case, it does not work on a string of numbers:
>>> int("123 890 123")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '123 890 123'
> Before we can select a function to convert a string into a number we must first pick a numerical type and before we can do that we must think real hard about how big of a number we're going to support in this application
Aren't you doing the exact same thing in Python by picking `int()` over `float()`?
> what type of string are we going to get from the command line?
Well, this might be the case for system languages, but most have a single `String` type.
> Before we can select a function to convert a string into a number we must first pick a numerical type
Except this has literally nothing to do with static typing and everything to do with whether or not the language you're using cares about numeric types.
For instance in typescript you'd just have `number` as the type. You could conceive of a dynamically typed language like python where adding a u8 to a u32 would cause a runtime error. In which case it sure would be nice to have the compiler tell you to convert before you called a function.
>The other problem is that when you write such trivial programs
If only I had a career writing trivial programs that would make a lot of things easier.
First of all: I always have and always will be a proponent of statically strongly typed languages (although I would argue that "statically strongly" is redundant here, there are only typed and untyped languages, IMO).
But there's one cost that the author seems to miss and that I didn't fully appreciate for some time. That's the cost of composition. If you look at python you can pretty much compose all possible packages on pypi in your project. All their means of abstraction are usually fully compatible (except for concurrency). Composition often only requires a little bit of glue code.
In a statically typed language's ecosystem you will probably a handful of fundamentally different means of abstraction like functors vs. classes that simply don't compose well together. Hence the fragmentation increases massively.
People are saying strong static typing is not a worthy investment mentioning their experiences with Typescript. That's Typescript. Types are comments. They don't even exist in runtime. Try a real strong static typed language like Rust.
Dynamic typing is fine for exploring. If I'm trying out a new API and don't want to look at the documentation every 30 seconds (or more realistically the documentation is terrible) then writing a simple service in Python is a great way to gain experience. Someone mentioned Julia; I think that most of the stuff you're going to be doing with Julia is exploring so it's fine that it's dynamic. But, for anything actually intended to be used, all the advantages of strong static typing come into play. The problem arises when people start out exploring a problem with a dynamic language, get something working, and then try to expand that dynamic program into something production-ready.
This article might be right in an academic sense. I suggest the author and those who believe similarly that its a "hill to die on" rethink their approach and spend 5-8 years doing professional consulting (do not confuse consulting with staff augmentation). You will indeed die on the hill as a business cannot expect everyone it hires to believe the same thing, and you cannot expect to force anyone to use strong typing properly. In short, while you may believe this is the right path, most folks dont care, and all that remains is an extremely complex, antipatterned, mysterious ball of mud that no one wants to work with because no one fully bought into the concept for which you were willing to die.
> There are definitely uses for untyped languages (or language variants), for example they are much nicer when using a REPL, or for throwaway scripts in environments that are already hopelessly untyped (e.g. the shell).
I think there is strong religious orthodoxy surrounding static typing in computer science, and this article reflects this religiosity. Strong typing proponents often fail to acknowledge alternative beliefs which assert that strong typing paradigms can be less effective and efficient than dynamic typing for many software development contexts. Rich Hickey, the creator of Clojure, is a fairly prominent counter-voice of this debate and provides some interesting context in this presentation: https://www.youtube.com/watch?v=2V1FtfBDsLU&t=2227s
The best engineers I’ve met are business objective driven. Strong vs dynamic typing is often a secondary concern. I understand the arguments for each approach, but I’ve never understood those who are dogmatic about their preferences.
Seemingly random changes between float64 and object happen when doing unit testing(pandas).
Can't remember the exact situation because I switched to While permanently, but there was some For loop problem. If the data came in as length 1, the for loop would read the individual chars of the string. If it came in as >1 it would iterate through the list.
15 years ago when I started python, maybe it made sense. Probably ~5-6 'oof that was a bug I wasted time' in 3 years professional use. Today, I find myself forcing types to solve a bug, or at a minimum checking the types as the program progresses.
Pandas's flexibility is amazing for exploration and prototyping. I'm actually a huge fan of the gradual typing approach, whereby you can start just messing around with stuff in a notebook, and then add the types and the rest of the structure once you are ready to solidify your design.
df.ix[0]
df.loc["x"]
df.loc["x", :]
df.iloc[0]
df.x
df.x[0]
df["x"]
df[["x"]]
df[df["x"]]
df["x", 0]
df["x"][0]
df[:, 0]
# and probably a few more I forgot
If you stop working with it for 2 months you forget what each one is doing and when you are supposed to use it.
I'm not clear what your argument is. Many of your examples are just syntactical sugar which you can very easily ignore, and even prohibit in your linter.
Also, note that pandas's datafrimes shine when you focus on column operations, if you're finding yourself accessing individual elements by index often, that's usually a sign that you should switch to a more appropriate data structure.
Looking at random people's code online is like staring into the abyss. I sometimes see APL code and find myself running in circles and screaming for a bit before I regain my composure, but I don't think that's reason enough for me to complain about APL's design.
There are alternatives to Pandas, for example Polars doesn't have this problem.
Sure, doing an operation in Polars can be more verbose, but it really does have just a few ways of doing things. Which in the end is actually more productive, since you don't need to always google the syntax.
When I sit down to do a side project these days I’ve gotten into the habit of turning on a screen recorder then just forgetting about it and cracking on with the project. Watching these recordings back at 10x speed has been fascinating to see how I work.
Most of my findings around wasted time and effort or correctness stem from design (faulty or skipping design analysis altogether and just coding). Every other aspect of coding just pales into insignificance compared to this one thing.
I'm also on the side of static typing being beneficial. I would always however consider the case at hand individually rather than reject it offhand because it lacks it. e.g. a complex distributed working system written with say Elixir or Erlang isn't easily replaced with typescript. Not all systems are better with any static typed language than any without.
Once you reach a certain size of developers, add static typing if available, e.g. TypeScript, Sorbet, etc
I agree, and I think the industry does too. Types are being retrofitted onto languages, like PHP, or added as a superset of the language, like with Typescript.
But in my experience I have also experienced the opposite. I have seen people casually reuse variables by reassigning them to a new type or developing a single function that returns different types. With dynamically typed languages it sometimes feels like you can take messy shortcuts because they let you.
It seems like such a weird thing to have a super strong opinion on. Sure if you only ever do one specific thing maybe there's an argument to be made that one is better than the other. I think there are just some problems that are best solved with statically typed langs, and some are better solved with dynamically typed langs. IIRC research shows it's a wash.
Just spotted a typo "mulitple" in case the author lurks in the comments here :).
("You'll pay it mulitple times over when you need to debug why your code doesn't work (either locally, in the test suite, or in production).")
I don't care how many times I see this argument; strong typing is correct. We need to keep beating this drum because every year there are more and more new developers that know nothing. We need to make good one that aren't ignorant of history and what works...because the more things change the more they stay the same.
I don't have much to say about the piece itself other than express broad agreeement on what is a fairly common sensical topic, but just gonna say it's interesting how many commenters here are up pontificating on their soapbox while still clearly confused about the distinction between strong and static typing.
The author leaves out what to me is the most compelling argument against static types: it is somewhat at odds with interactive development with a running system, as seen in smalltalk and lisp.
Now, not a lot of developers are really doing this. But it's still a good reason for those who are.
We want to identify and fix problems as early as possible; ideally, before we even write them. Problems cost exponentially more to fix the later you catch them, and the damage they can cause only grows with time. Static typing lets you identify problems earlier than dynamic typing, full stop. In fact I'd rather have strong static typing and 0% test coverage than dynamic typing and 100% test coverage -- in the latter case, I'm catching problems much later, and I have the maintenance burden of all those tests, 70% of which are only there to catch what static typing would have already found.
The best mix for me is to lean heavily on the type system and only lightly on tests, trying to write code that is simple and "obviously correct". Obviously-correct code has already had its problems ironed out in the first 6 or so bullets below, and there is very little room for further bugs to hide. Of course even "obviously correct code" can actually have bugs or be subtly incorrect -- a better definition is that any surprises in such code would also be surprises to the tests. The tests are nothing more than mirror images of the code, and therefore are not helpful or necessary. Static typing helps me think about the problem clearly, and helps me write "obviously correct" code.
Here's my working list, in order of preference of when I'd like to find the problem. Every time you drop down this list, the cost to fix the problem gets multiplied by some small factor ~1.5 to 3.
- Initial ideation ["Facebook for dogs? That idea sucks."]
- Requirements analysis ["That's not actually how we calculate that metric."]
- Conceptual system design ["Wait, these parts don't fit together like that."]
- Low-level design ["Crap, a loop won't work here, I need a different flow."]
- Brain-to-fingers typing ["Whoops I almost typed 'elesif'"]
- Immediately post-typing [Red squiggly line under 'elesif']
- Re-reading ["Wait that should be i-1, not i"]
- Compile time ["SYNTAX ERROR: Unknown identifier 'elesif'"]
- Unit test time ["Assertion failed: expected 7, got 8"]
- Code review time ["You didn't handle the case where N is negative"]
- Merge / integration test time ["Oh crap, David's commit broke my commit! How the hell do I merge this?"]
- Internal manual testing time ["You need to tighten up the graphics on level 3"]
- Production ["The users say the application keeps crashing when they run this report! Fix it!"]
- Years later/never ["It turns out that the analysis code we've used for the last 10 years had a major bug in it the whole time and we've been giving wrong numbers to a regulatory agency that has the power to shut down our company."]
> Problems cost exponentially more to fix the later you catch them, and the damage they can cause only grows with time.
Citation needed.
This is ENORMOUSLY context dependent. Enough to invalidate the entire point.
My day job is writing code that runs on a server I control. Mistakes there are super cheap to fix. I get a nice crash in sentry with stack and variables for all frames, I fix it, deploy. Done.
If I was shipping code to Mars, well that's the opposite situation.
>> Problems cost exponentially more to fix the later you catch them, and the damage they can cause only grows with time.
> Citation needed.
Really? Isn't this one of the most clear and repeatedly demonstrated facts in all of software engineering? We don't have a whole lot of clear-cut demonstrable facts in this industry, but this is absolutely one of them.
How about this 2002 NIST report, Table 1-5 on page 1-13 is "Relative Costs to Repair Defects when Found at Different Stages of the Life-Cycle" and shows 1X at "Requirements", 90X at "System Testing", 440X at "acceptance testing", and
up to 880X at "operation and maintenance".
You can find thousands of people saying the same thing from their personal experience with a quick Google. On the list of uncontroversial statements about software engineering, this is right at the top. Is our discipline so immature that we can't even agree on this extremely basic fact?
Another (okay, they don't say "exponential" here, or try to quantify it at all):
> The cost of finding and fixing bugs or defects is the largest single expense element in the software lifecycle. ... The earlier in the development lifecycle defects are found, the more economical the overall delivery will be.
The Cost of Poor Software Quality in the US: A 2020 Report, Consortium for Information & Software Quality
> My day job is writing code that runs on a server I control. Mistakes there are super cheap to fix.
That's good to hear, but mistakes caught before they get to your server are even cheaper still. Perhaps your exponential factors are smaller than NASA's, but it still clearly costs you much more to fix a problem that made it to your production server (and possibly impacted your users) than to fix it immediately after typing it because your IDE showed you a red squiggly line.
> Really? Isn't this one of the most clear and repeatedly demonstrated facts in all of software engineering?
It's a repeated saying.
> You can find thousands of people saying the same thing from their personal experience with a quick Google.
I know.
> That's good to hear, but mistakes caught before they get to your server are even cheaper still
That's a nonsense statement. If they are caught AT NO COST they are of course cheaper yea. But that's the thing. It's all tradeoffs. Everything is about tradeoffs. What is the marginal cost of finding bugs?
> than to fix it immediately after typing it because your IDE showed you a red squiggly line.
Yea, that's nice. In that case the marginal cost is approaching zero. But how many percentage of all bugs is that? And what is the cost?
How many bugs will my IDE find if I switch from Python to Haskell or Rust? And how will that impact my development speed?
I write my frontend in Elm because that's code I ship to users (via the browser) and THERE the costs are enormously worse, often because the problems can be seen by customers and not internal users (again: CONTEXT). I'm willing to tolerate a big productivity cut for being sure the code is correct there. But on my server? Nope. I am not willing to leave an easy order of magnitude of development time on the table for that type of safety.
And why stop at static typing? Why don't you run ALL your code through Property Based Testing AND Mutation Testing 100% before shipping? What's the marginal cost of catching a bug then? Why not run ALL your code through a theory prover like Coq?
Bugs are NOT always "cheaper" when found early. It's about the COST of finding the bug early. Of course it is! If the cost is a trillion dollars in programmer time to find a bug that takes 1 second to fix in production at a cost of a few dollars of programmer time, by definition it was NOT worth it.
It's ALWAYS a tradeoff. Of course it is. Everything is.
It's maddening to me that people refuse to see this when it's so blatantly obvious.
There's a reason I used the word "problem" and not "bug" in my original post. In fact every time I found myself writing "bug", I deleted it and wrote "problem" instead. You're thinking very specifically about "bugs": clear spots a codebase that are clearly causing some clear error. That's actually a small percentage of the scope of "problems" that I'm talking about. Problems can be: your idea is bad, your architecture doesn't fit together, your performance is bad, your calculations are wrong, etc. Static typing, when done well, can help prevent many more of these issues than that narrow definition of "bug". Static typing helps you think clearly about your code.
> the marginal cost [of fixing a red squiggly] is approaching zero. But how many percentage of all bugs is that? And what is the cost?
The marginal cost of fixing a red squiggly underline is indeed basically zero for typos, and typos are a class of bugs I'd like my static type system to help me catch; and it does, and dynamic type systems generally don't. So the percentage of typos that become bugs is basically zero, but that's partly because of static typing! When you say "the percentage of all bugs", your definition of "bug" is a-priori excluding things that were caught before they became bugs!
But you can also get a red squiggly underline because you thought some variable was in scope when it's actually not -- that bug could have been caught earlier. The cost of fixing that bug is not near zero: the structure of your program is different between your brain and your code. The easiest bug to fix is the one you never write in the first place.
Or you get a red squiggly because you thought you could pass an object of type X into a method you're relying on, but you actually can't. Dynamic typing will not help you with this, and the cost of fixing that bug is not zero: again, your assumptions about how your program is fitting together are incorrect. I'd much rather realize that I can't use a method in exactly the way I thought right at the moment of typing it than later during test time. The latter requires writing and maintaining a test in order to catch a problem late that I could have caught early with no test.
> And why stop at static typing? Why don't you run ALL your code through Property Based Testing AND Mutation Testing 100% before shipping?
I directly addressed the subject of testing in my original post. Static typing allows me to write many fewer tests than dynamic typing.
> If the cost is a trillion dollars in programmer time to find a bug that takes 1 second to fix in production at a cost of a few dollars of programmer time, by definition it was NOT worth it.
I just don't understand this hypothetical. Why would it ever take a trillion dollars to prevent a bug that takes 1 second to fix in production? This just doesn't jive with any developer experience I've ever heard about, any example I've ever read about, or anything I can even imagine.
You seem to be operating under the assumption that introducing static typing makes you slower at programming, but we're claiming it's worth it. No, that's not my claim. My claim is that above some level of program complexity, static typing makes you strictly faster at programming, full stop. That level of program complexity may not be your internal server or whatever, but it definitely is most production-grade software out there.
> It's ALWAYS a tradeoff. Of course it is. Everything is. It's maddening to me that people refuse to see this when it's so blatantly obvious.
This is generally true in engineering, but not always. It is actually possible to simply advance the discipline. When you're building a suspension bridge, there's a tradeoff between Stainless Steel and High-Carbon Steel, but there's not a tradeoff between Steel and Popsicle Sticks. Unless, of course, you're building a toy or a proof of concept. Dynamic typing is fine for toys and proofs of concepts; nobody is disputing that. Professional engineers don't spend most of their time discussing the best way to build toys.
> Problems can be: your idea is bad, your architecture doesn't fit together, your performance is bad, your calculations are wrong, etc. Static typing, when done well, can help prevent many more of these issues than that narrow definition of "bug".
I don't see how static typing helps with "solving the wrong problem" kind of issues at all. That's what asking deep questions about the business is for. Maaaaybe DDD can do something here, but that's hardly "static typing".
> I directly addressed the subject of testing in my original post. Static typing allows me to write many fewer tests than dynamic typing.
I noticed here that you didn't make any difference between testing, PBT, MT and proof systems. That's pretty bad. I won't conflate Haskell-style good type systems with C-style horrible type systems, so you shouldn't do the same with testing.
> I don't see how static typing helps with "solving the wrong problem" kind of issues at all.
One example is F#'s unit of measure system, which can statically verify dimensional agreement of arithmetic. Another example would be differentiating between "Safe Strings" and "Unsafe Strings" (say, untrusted user input) and statically enforcing which are allowed in which method. They won't tell you that "Facebook for Dogs" is a bad idea, but they may reveal subject-matter errors in the actual requirements themselves. Most test suites wouldn't even catch these errors, because test assertions are generally based on requirements. You could write your own unit-verifier that you run in tests, but then you're just reinventing static typing. In fact it's a common criticism of test suites in dynamically typed languages that they end up poorly reinventing checkers for the guarantees already provided by a decent static type checker.
> I noticed here that you didn't make any difference between testing, PBT, MT and proof systems
I also didn't differentiate between the Late Middle Ages and the Early Renaissance, because it was off topic. But if you insist:
"Property-Based Testing" is a testing pattern, not a different kind of test. Write tests and assertions as appropriate for the thing you're testing. It seems to me that you should have enough understanding of the likely failure scenarios and edge cases of your code that you don't need to be randomly generating inputs, but I can see how it'd be useful. I test all sorts of invariants in my tests. This just doesn't seem like a novel thing to me.
"Mutation Testing" is basically testing your testing methodology. Okay. You could also test your bus factor by randomly shutting out one developer for a day each month and seeing how you get on. Maybe good ideas for some companies? If you find yourself trying to develop a completely airtight testing process that will catch 100% of bugs, it's probably because too many bugs are passing your tests. Before you kill yourselves trying to develop an airtight, mathematically-guaranteed strong test system, may I suggest strong static type systems instead? Just let the compiler do it.
Self reply, in case it was unclear: I don't claim that testing is only for weak type systems. Testing is important in both worlds. I do, emphatically, claim that the total testing burden is much lower for programs written in strong static type systems, because a majority of tests written for programs in dynamic systems are simply trying to cover for the lack of strong type checking. So of course testing paradigms apply to both strong and weak type systems.
>The question around strong static typing is simple: would you rather work a bit more and get invariants checked at compile-time (or type-checking time for non-compiled languages), or work a bit less and have them be enforced at runtime
The dynamic typing crowd is frankly just flat out wrong at this point and someone has to say it - so, thank you Tom. This is especially apparent with advancements of type inference in compiled languages.
Beyond initial prototyping (which I doubt will actually ever be thrown away), what logical defence is there for catching obvious bugs in production when they could have been caught at compile time instead? Dynamic typing straight up hides unavoidable realities of programming - you have to know what thing you have to cover all the cases. I don't just mean cases that "make the compiler happy". These cases are most often not mutually exclusive from the business ___domain - cases related to core business logic are missed, which produce more bugs which makes your software objectively worse for both the end user and whatever business the software is sold by.
The complaints I hear are that it's too cumbersome or annoying to satisfy these cases - my retort is to stop complaining because most of us are paid to write working software, not software we can pass off as being complete when it's not.
And don't suggest that tests cover this - they absolutely don't, and testing such things is a total waste of time. The train has left the station, best get aboard.
Dynamic typing triples your software development speed and decreases bugs in delivered software by two thirds.
You can either hire 10 dynamic typing software developers or 30 static typing software developers. Businesses which choose to hire 30 static typing software developers will fall to the companies which hired 10 dynamic typing software developers. It's pure economics.
Static typing fills your codebase with bugs. Whilst an inexperienced software developer might not understand why static typing increases rather than decreases bugs, you can see the increase in bugs in research papers on the topic. As plan as day.
(Hint: You have made your code more complex by using static typing and more complex code contains more bugs.)
No one is against static typing. This thing that some people, myself included, oppose -- with good reason -- is mandatory static typing. The idea that anyone advocates giving up the ability to catch bugs at compile time if it can be done at no cost is a straw man.
I am arguing for it, for one. There isn't a single good reason that it should be optional in a production code base moving forward. If you started with a dynamically typed language, you should be introducing type hinting and enforcement at a CI/CD step as soon as yesterday.
All I'm pointing out, is that people who oppose strong static typing on the base of "but we don't want it everywhere" are drawing the wrong conclusions, because, indeed, we don't want it everywhere. We (I) do, however, need and want it in far more places than it's used today.
I would argue this is a false equivalency. The explanation is that Python is too entrenched to replace, not that it is somehow superior for this use case because it is dynamically typed.
Well... I'd phrase it like "no statically typed language has proven it is superior or even applicable for this use case yet". You can't say Julia hasn't given it a serious try and hasn't gotten first-rate marketing treatment by being name checked in "Ju-Py-Ter" notebooks and other placements. Instead, Julia has serious deficiencies
Ok so, based on the comments it's basically people who loves Strong Static Types and people who only experience with types is Typescript and they hate it.
Hmm, just a few years ago, before typescript, the prevailing wisdom was that types are dead, this is a brave new world out there, just need to create more unit tests.
Does mypy for Python offer a complete solution to introducing static type checking to Python? What is mypy missing that a language with strong static typing has?
How long until Copilot can type-annotate any piece of dynamic code, making the issue mostly irrelevant? Everybody sees the code how they like to view it.
Dynamic typing is polymorphism at the language level. In essence, you expect most operators to behave sensibly with different types. An expression such as "a + b" resembles a polymorphic function such as "add(a, b)". You would expect it to work fine most of the time, regardless of the types. And the types in a dynamically-typed language usually aren't so many – you possibly have booleans, numbers, strings, lists, maps, sets and objects.
I understand that modern type systems can do anything (that can be computed) since they are Turing complete. So are Java generics and C++ templates and Lisp’s macro systems, but I don’t want these things.
I’ve written a number of Turing machine programs; yeah it’s impressive that an imaginary machine that is so simple can run any program that runs on any other computer, but it’s no fun writing code for a Turing machine. For the Theory of Computing studying Turing machines is important.
If I don’t want Turing complete type systems, what do I want? Well, first off, the bugs I find in my programs that are more troubling don’t seem to be stopped by strong type systems. What I want is to be able to declare assertions in my code. For example, I’d like to tell the compiler that the variable year must always satisfy (2000 < year < 3000) and that in a certain block of code that x and y are always within 0.5 of each other. I want the compiler to ensure that these invariants are satisfied by a combination of compile time and run time checks generated by the compiler.
The problem is that we can’t expect programmers to use Turing machine like type declarations, because the programmer must now program the type system without bugs to ensure that the types in the program carry enough information that higher level assertions can be made about the original program’s correctness.
TLDR, I want to work with higher level assertions about the code than that provided by complex types.
At this point it's beyond a hill I'm willing do die on. I'm not really interested in discussing it. If you don't get it I probably don't want to talk to you about it, I might even think less of you as a software developer. The amount of pre-existing respect for someone I'd need to have before I engage in a good-faith discussion on "are types good" is pretty high.
edit: To clarify, I am on the "types good" side of things
I don't consider it productive to be personal about technical topics. It's best to divorce yourself and the other party from the issue to limit as much bias as possible. You don't need to worry about the good-faithness of an objective, egalitarian discussion. It's never truly objective, but you can at least catch yourself saying things like "I might think less of you as a developer" and recognize that's a preconceived notion. In some cases, it may be turn out to be accurate, but it's definitely not always. Dynamically typed languages have had an important role historically in software. We should not categorically denounce people who prefer it as lesser developers. I also generally prefer static types.
Some prejudices are rational. To paraphrase, it would take an enormous amount of prior trust and respect before I even humour someone trying me to convince me the sky is green with a straight face.
Some issues are settled enough that there is no need to discus them any further. It is okay to automatically mark people on the wrong side of such issues… let’s say ill-informed.
Static typing, I believe, is close to being one of those issues.
I think this is OK as long as you keep your mind open to situations that you have not considered.
Imagine someone is working in a relatively niche new programming language ecosystem which is dynamically typed, allowing the language to have some richness that modern type systems don't support. I don't have an example because I don't know of any such language in 2023... BUT back in the 70s and 80s this would have been Lisp. Lisp couldn't have been strictly typed back then because, AFAICT, type systems hadn't advanced enough to express the sort of metaprogramming that made Lisp unique and awesome. This was at a time when most popular languages were strictly typed.
I would hope that you would keep your mind open to whatever that maps to in the 2020s.
Now, if someone says "I like JS over TS because types are annoying and slow me down", then yeah, I don't have much patience for that either.
You make points that seem great to me although I know almost nothing about Lisp or 20th century programming.
The thing is, I've yet to encounter a single instance of such an argument today. Every single time it ends up being "I like JS over TS because types are annoying and slow me down". It hadn't even occurred to me that laziness and sloppiness weren't the the only reasons to write in dynamically typed language.
I suppose what I'm saying is I'm quite interested in seeing what kind of evolution some dynamically typed language could offer in the future. Although with no signs of its coming, I'm going to stick to TS because it's objectively better for anything but very small projects.
TS is an interesting example to pick when trying to argue that types are better. You have all the same downsides people pick on when arguing against strong, static typing, but since the type system isn't sound you still can't rely on the input to a function being the type you expect (and the problem is worse anywhere you inevitably interact at least a little with the JS world outside your TS bubble).
At best it's a form of documentation that enables some simple linting rules and a bit of jump-to-definition magic. Like, that's a benefit (mostly -- I tend to think that type signatures implying more guarantees than they provide is a recipe for inadvertently relying on falsehoods), but it's not as clear-cut of a win as you see in other statics/dynamic tradeoffs.
> The thing is, I've yet to encounter a single instance of such an argument today.
I don't mean to be unnecessarily contentious, but isn't that the point of undiscovered territory?
We haven't encountered it yet, when we do then as awareness grows that pattern, idiom, theorem or concept will be picked up by mainstream statically typed languages.
It's enough to believe that the properties of (for example) JavaScript might lead to an as yet undiscovered pattern that is not possible in current statically typed languages.
Yes sure, it's always possible. My narrow experience leads me to believe, however, that some necessary threshold hasn't been crossed yet. And there's vanishingly little chance that I personally will be participating in such a revolution. Therefore from my perspective, it isn't happening yet and I can only wait to see if it does - meanwhile, I'm on the opposite side.
Imagine working on a networked system that uses types to make it impossible to express operations that violate data security and integrity constraints, and where such constraints control whether data is allowed to be read or written from or to a given endpoint. Imagine further that the types governing permission to read or write depend on environmental state that can change at any time--for example, as soon as a specific person changes roles, the set of ports they are allowed to read and write changes.
The type system enforces those permissions: writing to an impermissible destination is a type error. The types applicable to an entity are not necessarily knowable at compile time; some of them might change at any time.
I had a job working on such a system. It supported a type system implemented in Haskell with both static and dynamic type disciplines, where values were tagged with base types designed to be checked dynamically in hardware.
Was the programming language dynamically typed? Yes. Was the programming language statically typed? Yes.
Python fits that niche now. Half the way tensorflow, et al work is by doing brain surgery on the AST in a way that resembles hey day Lisp's 'even the code is just s-expressions, process it as much as you want to the point of 80/20ing your way to your own compiler'.
It sounds like you have an implicit prejudice against those who do not "keep [their] mind open to situations that [they] have not considered", since the way you phrased your concern is moral in nature. You see it as morally bad not to have an open mind in that way.
Yeah, sure, I do in fact think it is virtuous to be aware that we don't always have complete information and that we should be willing to revise our opinions when new information is encountered. Is that controversial?
No, no, not controversial. I intended to make it easier to understand the point by demonstrating for the more literal-minded that using moral language sets up two halves of a dichotomy, one preferred to the other, so that such prejudices needn't be personal but rather merely moral. The difference is of course that which side of a moral dichotomy is a choice that can be reversed (e.g. making an effort to open the mind) while personal matters are immutable in those people.
If your prejudice prevents you from having a conversation with Matz or Jose Valim or Van Rossom or the creators of Julia about software development, you may want to reconsider your prejudice as a hard and fast rule.
You can't honestly believe no one who likes to develop in a dynamically typed language has nothing interesting to say about software development?
I can't (or, more accurately, don't) believe that, but I can certainly believe they have nothing interesting to say about the topic of static vs dynamic typing.
An open mind is like a fortress with its gates unbarred and unguarded.
It is both useful and allowed to have a certain level of belief that won’t be crossed without heavy effort. The OP didn’t even say they were closed to the conversation completely, but that a random person with no pre built trust isn’t going to get the time of day from them to rehash the same settled argument.
Doesn't everyone have those? Like, I'm unlikely to really engage with someone about my health unless they're a doctor or otherwise have some area of expertise. Or if it's just a casual, friendly conversation, to kill some time.
Everyone has biases. But you aren't describing a bias, you are describing a refusal to engage with people below you. Speak for yourself, I don't have that.
We’re all humans and life is very complicated with many facets. If we all looked hard enough I’m sure we could find a person you’d refuse to engage in a debate with over their “thing.”
But that wouldn’t be a wise use of time in my opinion. And that’s what they’re ultimately driving at: we all have limited time to expend, so do it in things that matter to you. I believe “matters to you” is a bias.
"Below you" is a bit of an exaggeration, I think. I said that I might think less of someone as a software engineer if they express an opinion that I think is particularly bad. Hardly egregious, in my opinion. I also might not - it frankly depends a lot on the situation.
These people opinions are based on bad information.
Beliefs inform decisions and other beliefs
I disagree with them on a huge number of fundamental things in software. Most of their fundaments are claims with no evidence. Due to that, I simply do not care about what they have to say most of the time.
Python got type hints bolted on. A type system for Elixir is being worked on. Dynamic typing was a prerequisite for Erlang to enable hot-code-reloading. Julia is can be typed for an extra speedup. Stripe built a type checker for Ruby called Sorbet.
I'd love a dynamically typed scripting language with the following two properties: no "import" mechanism to split code over multiple files, and no way to execute more than (say) 256 lines of at most 128 bytes each in the file.
Dynamic types are nice for quick & short scripts, and actively detrimental for anything long and complex. Why not enforce that? As soon as it gets so long it doesn't run you know it's time to rewrite in a statically typed language. Instead all existing scripting languages allow unlimited growth in code size, making it easy for programs to grow beyond the point where the language is useful.
> Dynamic types are nice for quick & short scripts
A long list of big products and companies are there to disagree with this. I am not saying that dynamic is better or worse. But saying that dynamic languages are only good for quick short scripts is a wrong generalization that has not one exception but many exceptions.
> To paraphrase, it would take an enormous amount of prior trust and respect before I even humour someone trying me to convince me the sky is green with a straight face.
Don't venture into tornado country; your doubt may be your downfall.
> To paraphrase, it would take an enormous amount of prior trust and respect before I even humour someone trying me to convince me the sky is green with a straight face.
But people regularly say the sky is blue, and it's clear as day that it's not when the earth has turned and your side isn't facing the sun.
I don't think that's a statement that most would agree with. What is the sky? Sure the atmosphere can be called blue, but the sky is what's visually above the earth. At night, that's black space. The atmosphere is mostly transparent at night.
The sky isn’t inherently blue. It’s essentially colorless.
Only due to Rayleigh scattering do we perceive it as blue, but that’s not due to the absorption and reflection of different wavelengths we associate with innate color. Note that the color changes depending on the angle of the sun, even to the point that it’s purple and red at a few times during the day.
I explicitly said "as a developer" because it's not a personal judgment. You can be a good person and also have a terrible take as a software engineer.
> We should not categorically denounce people who prefer it as lesser developers
I don't think you've justified this point. I'm comfortable with my position on this.
What you say is true, but it’s also true that the person you are replying specifically made their remarks about productivity - you’re both talking about what is best at work.
It seems reasonable to argue that statements that you won’t even discuss X any more and would rather judge the person as less competent professionally are unproductive. Not saying I agree, btw. But it does seem like a pretty basic point. One of you is talking about preserving your sanity and the other about output. It’s not necessarily a disagreement even.
I still think it's reasonable to argue that's not a "productive" approach, but like I said, that's not my own opinion, I just think it's a reasonable argument.
Again, I don't mean "personal" in the sense that you are making a statement about someone's worth as a person. It's "personal" because you extrapolate information about an individual person's technical skills from a single opinion than you have any real factual justification to do so. People will always find ways to defy your preconceived notions.
I don't necessarily think it makes someone a "lesser developer" if they prefer untyped languages, but I DO think they've either had to maintain anything long term or it stayed relatively small.
Whether that makes them lesser or not isn't really for me to say, but I can say I'm definitely on board with the idea that types increase productivity the longer a system is maintained. Unless used poorly, types don't automatically mean you use them well, but they make it a hell of a lot easier to do the right thing.
I think you're being a bit too sensitive, and it's not personal. Someone is espousing a (bad) technical opinion about their field of work, it's not unreasonable to say you respect them less in their field of work. That's not personal.
It's like if you met a builder who refused to use a hammer and insisted on bashing nails in with the back of their drill. It's not "personal" to say you'd respect that person less as a builder, regardless of how much you'd enjoy having a drink with them.
It is "personal" if you attach someone's technical opinion to broader implications about their own competence. If you disagree with a technical opinion, say so and move on, there is no reason to even discuss someone's own personal skillset, experience, or value as a developer. It's a silly fallacy to automatically label people who disagree with you as incompetent. All it does is foster bias and stifle actual discussion. The responses to this post are evidence that this is not as cut and dry as the original posts suggests, so I suggest we try our best not to cover our ears and embrace tribalism just because we think less of someone's opinion. I don't think I'm being oversensitive by saying this is unproductive dogmatism. It is my honest opinion, yet I do not extrapolate to mean anything about the proponents' value as a developer. Unconscious bias is a pervasive problem for everyone, especially when it comes to binary holy wars like static vs. dynamic types. This is more akin to a builder who uses a nail gun rather than a hammer. Hammer enthusiasts can either acknowledge that both approaches have tradeoffs or they can petulantly insist that people who don't use their methods aren't "real" builders.
I'm sorry but this is a discussion about their competence. You can't separate someone's opinions on technical topics from their competence in the very technical field you're discussing. If a cartographer has the "opinion" that the world is flat then that directly speaks to their competence as a cartographer.
For starters, "the world is flat" is falsifiable and trivially disproven with evidence. "Dynamic types are better" is neither of these. If you're going to pin someone's professional value to a single technical opinion, you should at least be able to back it up with data.
Nah, someone opposed to typing is either a shit developer and/or a madman who codes too clever by half mad shenanigans that take advantage of the lack of strong typing. I wouldn't want to have to maintain or depend on code written by either.
For the very rare cases where a lack of strong typing is needed, most languages with strong typing offer ways to handle that (i.e. the Object and Dynamic types in C#)
As someone who usually prefers JavaScript over TypeScript but has used many typed languages over the years and generally finds then easier to work with in a way, I agree that one should not pre-judge based on something like that.
But he's just being honest. Many developers have been making that judgement for many months or even years.
This is basically where I am at. Most libraries and applications are trivially typed even in languages with relatively poor type systems like Go and Java. The necessity, or even benefit, of weak and/or dynamic typing is extremely rare and most languages have workarounds or escape hatches for those exceedingly rare cases.
Dynamic typing also makes a ton of optimizations basically impossible and even after monumental efforts languages like Javascript are still quite slow, inconsistent and memory inefficient outside of trivial benchmarks.
The take I have is that with a dynamically typed language you still have a static analysis step. It’s just that the analysis happens in your and other developers’ brain and it is objectively worse. I honestly can’t think of a single thing in favor of dynamic typing.
I write Clojure in my day job and it’s insane how often we have issues where it would have been immediately caught by a static type check.
I'd love to learn more. Are you part of a large team (enterprise or SMB, whatever you can share)?
How have you experienced using Type Clojure, spec, Malli, etc. to determine correctness?
I've only worked on solo projects with Clojure, with most of it fitting into my head. I imagine with teams of size N > 1 things can change quite a bit.
> Javascript are still quite slow, inconsistent and memory inefficient outside of trivial benchmarks.
I thought this meme was dead already. Of course, you might not be able to squeeze out the same amount of performance compared to a brilliantly written C or Rust program, but for what it is, JavaScript is pretty damn fast already.
DOM manipulation on the other hand, is still a very common bottleneck people come across when writing typical JavaScript code.
> but for what it is, JavaScript is pretty damn fast already.
It's fast compared to other dynamically typed language implementations but it's still very slow compared to basically all of the popular statically typed languages.
I think what cracks me up about this conversation is that it's an almost verbatim repeat of a conversation I had on reddit a few weeks ago.
There's a certain segment of the developer population that I don't think realizes just how fast C and C++ are. Javascript is _relatively_ fast when compared to other dynamic languages, but not when compared to C, C++, FORTRAN, etc.
> It's fast compared to other dynamically typed language implementations
true
> it's still very slow compared to basically all of the popular statically typed languages.
Not true.
The main slowdown for javascript (AFAIK) is the checks the optimizer has to put into place to ensure the assumptions it's made about the type are still valid. If, however, those assumptions are valid then javascript ends up emitting pretty much the same assembly that you'd see for and highly optimized statically typed language. In fact, there are some circumstances where it can beat a language like C++ or Rust due to the fact that it has to incorporate runtime information into optimizations.
With C++ or rust, if you add dynamic dispatch, unless you are doing PGO and whole program optimization, you are pretty much sunk with 2 memory lookups on every function call. This is the case where javascript can end up beating C++/Rust.
(All of this is talking about hot code after warmup. During the initial execution javascript will almost certainly always be slower).
Part of the proof of this was asm.js, the precursor to wasm. V8 at the time it was introduced could execute asm.js nearly as fast as what firefox could do with it's optimized asm.js compiler. That is, when you stripe out all the actions that make javascript slow, it very often ends up being just as fast as a compiled language.
What stuff ends up making it slow? Generally speaking, stuff that makes the types unpredictable (adding fields, removing fields, sending in a number and a string and expecting the VM to be able to handle both).
You can see a lot of this writeup around the discussions about why Dart was originally "optionally typed". Basically, the entire selling point to make dart fast was simply to remove the abilities to dynamically change types like you have in javascript. With that, the VM authors at the time were capable of making a VM that's every bit as fast as what Java has.
> That is, when you stripe out all the actions that make javascript slow, it very often ends up being just as fast as a compiled language.
ok...
> In fact, there are some circumstances where it can beat a language like C++ or Rust due to the fact that it has to incorporate runtime information into optimizations.
People used to make the same claim about Java and in every single example I've ever seen the Java/Javascript is extremely optimized, performance isn't consistent across VM versions, and the C++/Rust is extremely naive (usually allocating unnecessarily and not using arenas in hot paths that are allocation heavy).
None of these tests really show JS winning, but they are showing that it's within spitting distance of C++ in multiple tests [1]
And, you can look at the code yourself, most of the examples read pretty much exactly the same as their C++ counterparts.
Mind you, this is also a test that looks at execution start to finish and doesn't give warmup time (which will always favor statically compiled languages).
> performance isn't consistent across VM versions
That's true for C++ compilers, so why would you expect performance to remain constant with a JIT compiler?
> With C++ or rust, if you add dynamic dispatch, unless you are doing PGO and whole program optimization, you are pretty much sunk with 2 memory lookups on every function call. This is the case where javascript can end up beating C++/Rust.
GCC at least is capable of speculative devirtualization by using local heuristics, without PGO. And of course it is capable of devirtualizing in many cases when the knowledge actual type can be constant-propagated.
Also note that the vast majority of calls are not dynamic in C++ (as opposed to most dynamic languages), so devirtualization is significantly less impactful.
Fair point, AFAIK C++ templates are pretty heavily used to avoid a dynamic dispatch. Couple that with the fact that polymorphic OO has fallen out of favor generally and I could see this mattering a lot less now-a-days.
Trivial benchmarks where all of the significant data structures and networking are done in C++ and the tiny amount of Javascript is just passing some strings around. I guess it's "fast" if you can restrict yourself to little more than hello world where you do nothing more than pass a few strings to functions written in faster languages.
Notice that in the Java, C#, Go, Rust, Swift or Ocaml benchmarks almost all of the underlying data structures and much of the networking stack are built in the respective language. This is not possible with Javascript, Python, Ruby etc. because it would be ludicrously slow and extremely memory inefficient.
> Dynamic typing also makes a ton of optimizations basically impossible and even after monumental efforts languages like Javascript are still quite slow, inconsistent and memory inefficient outside of trivial benchmarks.
Please provide evidence for this extreme claim. I can point to benchmarks[1] where JavaScript is competitive with or even better than compiled static-typed languages like Java.
(any argument that those are "trivial benchmarks" is automatically invalid without actual empirical evidence)
Can you describe your usecase for which JavaScript is slow? There are many languages that are slower than js like python or elixir, but they are doing just fine, that's why I won't agree that js is slow, but sure there are cases for which js just wasn't designed, and any CPU intensive task will be slow, but there are ways to get around it as well.
I've worked with my fair share of vanilla JS devs who refuse to acknowledge the benefit of more statically typed variants of the language. Most of them bemoan the upfront cost of typing everything without realizing the benefits of it. I absolutely think less of them as developers.
Not liking typescript is not the same thing as denying that types are valuable.
TS has a very complex type system in exchange for relatively weak guarantees about correctness. That's a tradeoff you can choose to take or leave depending on the problem at hand, and holding that position is not the same as believing types have no value.
Unless you're talking about elm or rescript or something then sure sure. But usually when people say things like this they mean typescript.
This is pretty much my take, too. I don't hate the idea of TS, but TS is so painful to work with, and it's owned by a company I really don't like, too.
OP is saying it's provides weak guarantees which is a result of the fact they made it easy to adopt and implement among legacy untyped code. It's the opposite of painful to work with for typing systems, compared to something like Rust which has stronger guaruntees and more predictable patterns but hard requirements the full stack is type. The result in TS is plenty of "any"s everywhere and in legacy code some situations where it's mostly just a thing veneer of type safety.
But all of that makes plenty of sense because in the early days it was rare to work on a real life production JS project that was pure TS from day one in addition to having TS only libraries.
These days it's everywhere and the work has been done to move away from rampant anys in popular libraries. The tooling is also mature and it's rare to find major JS libraries not already packaged with types or a @types import. Turbo moving away from TS was the rare exception.
% cat a.ts
console.log('Hello, world!')
% time tsc a.ts
tsc a.ts 2.39s user 0.10s system 232% cpu 1.066 total
More than a second to compile a "hello, world" (or 2.4s in CPU time) is orders of magnitudes slower than any other compiler or interpreter that I know of. It's a ridiculous start-up time.
esbuild is not an alternative as it doesn't check types.
What I want is "GET /foo.js" to "just" compile TS "on the fly" in dev; it's a simple "just works" kind of setup, but not possible with TS.
Or for a simple system, just "roll your own":
for f in *.ts; tsc $f >|$f:r.js
watch-files *.js --run tsc
(for f in *.ts; tsc $f) | minify >production.js
Doesn't need to be shell, can be a simple JS script or whatever. You really shouldn't need millions of lines to call a compiler even in simple scenarios.
What exists now is an explosion of complexity to deal with all this and I guess these systems are "mature", kind of, but for a lot of systems it's massive overkill (and even for larger systems it's not particularly great IMO). Besides, it's really a bad fix for more fundamental problems.
Personally I wouldn't call TypeScript mature until compile times are roughly within the range of literally ever other compiler that has ever seen wide-spread adoption (and with that I don't mean "parallelize to 32 cores so my threadriper over 9000 can compile things in 0.1s). It doesn't need to be fast: just not a huge outlier.
You can still use tsc for type checking via editor language server and quality gates (with the noEmit flag) and get stupid fast compilation with swc or esbuild.
Explosion of complexity and "bad fix for fundamental problems" sounds a lot like you just have an axe to grind.
Many other popular languages have multiple build systems and tools to choose from as well; it isn't a particularly novel challenge.
I just want things to compile, with errors, like literally everything else works. It's really not a huge ask. I don't want complex bespoke setups with multiple compilers and background processes and whatnot. The classic JS/TS response is "here is the happy path with all this tooling, but if you want something outside of that then there is something wrong with you".
I guess the "axe" that I'm "grinding" is that I'm having a lot of difficulty using TS in a way that fits with my sensibilities and preferences. e.g. I don't like errors in my editor and prefer to explicitly call the compiler (for any environment). I suppose I could get all of this to work how I want it to with wrapper scripts or whatnot: but it's complex, time-consuming, and isn't needed for anything else.
Based on previous conversations about this at least one person will say something along the lines of "zomg what kind of crusty old backend unix beard doesn't use VSCode and want errors in their editor, you just need to get with the times as you're stuck in the past!!!1" but again: it works for literally everything else (including other compile-to-JS tools), and is not that "obscure" of a work-flow, IMHO, and we're back to a few paragraphs ago: "move outside the happy path and you're screwed".
I mentioned using tsc with the noEmit flag for quality gating, which is the same workflow you would use.
`tsc --noEmit a.ts` (possibly flipping the position of the flag, I forget if it is sensitive).
That'll check the types without spending unnecessary time compiling with the slower tooling.
From there, esbuild or swc binaries compile the code as desired. They use the same tsconfig file that tsc does, so no need for any extra complexity. They build so fast that you'll not mind having a separate command, I promise.
You could even combine the two into a simple one-liner if you wanted.
Compared to, say, java where you have to fight over maven or Gradle or ant, plus endless config options in XML or groovy or whatever, typescript really isn't much to complain about.
Hell, trying to set up a clojure full stack project is a nightmare of conflicting opinions over tooling between lein and shadowjs or whatever.
Of the big languages, C# is about the only one with a "one true way", and of the smaller ones, they simply haven't yet developed a big enough base to grow contentious enough to have divergent solutions.
The performance of "tsc --noEmit a.ts" is identical, and it makes no difference vs. just typing "tsc a.ts", and it doesn't really solve "compile on demand" (with type checks) in any case.
I looked in to this some time ago, because I couldn't believe it was this slow, and tsc just has a huge startup cost. Once it gets going it's alright (I think? Don't quote me) but to get started takes a long time. It's actually already improved because not too long ago it was more like 3 seconds (probably because of the parallelisation, which is kind of cheating IMO).
I don't know about Java as I never really used it, but complex build systems are not unique to TS of course, but what they are in TS is mandatory for any reasonable experience because it works around the fact the compiler is so damn slow. That's a huge difference you can get started without too much effort, and you can do "smart" things fairly easily as I mentioned in my earlier comment.
> they simply haven't yet developed a big enough base to grow contentious enough to have divergent solutions.
C, C++, Go, Rust, Python, Ruby, PHP don't have a "big enough base"? Ehhh
And my entire point is that TS LACKS "divergent solutions". Because it's so slow lots of solutions are simply not practical.
Yep almost every (serious) TS dev only uses --noEmit for development by default because they use VSCode/IDEs as their typechecking engines and then have something like esbuild for delivering the content to the browser locally. I've never had performance issues with either step and I use both daily.
The raw output speeds aren't a very good 'practical real world example' as the OP described it.
If anything in dev ESLint (+ Copilot) is the slow ones that I sometimes noticed, but there is already Rust driven replacements maturing in the pipeline as we speak.
If the tooling is a pain to use, slow, and owned by a crappy corporation, and I'm not going to use the types anyway, then I have even less of a reason to use it...
Typescript can be very complex - it can also be incredibly simple - it's up to you how far you want to take it.
Sure it can be an adventure to express something fully in it - but this is true for any type system I've seen.
And I haven't seen another type system that's integrated into a dynamic ecosystem as well (IMO python is 5 years behind in terms of type checking experience) and that lets you chose the level of type sophistication that makes sense.
Static typing, code analysis, automated testing, etc. are all great tools that become counterproductive past a certain point. Where that point is highly depends on what you're doing and typescript is one of the most flexible type systems at letting you make that choice. I'd say a lot of people are terrible at recognizing when they went too far with it for no practical gain.
My only problem with it is that they can't fix the shit JS semantics.
I am professionally familiar with typescript and explicitly not making a concrete argument for or against it right now.
I'm just pointing out that having an opinion about typescript is not the same thing as having an opinion about types. Something I think people are (intentionally?) conflating in these comments.
I think it’s just a path a lot of developers have traveled, starting with JavaScript and moving to TypeScript. If you’ve gone through that pipeline than you probably feel very strongly about the benefits that TypeScript provides and it’s the fulcrum for your opinion on static vs dynamic typing
Too many web developers, basically, I don’t think the conflation is intentional
I think you have to give typescript a bit of a pass in terms of types. MS had unique constraints when designing it originally, so it's never going to meet the expectations of more hardcore "typists", but I think they've done a great job given the unique challenges. Not perfect for sure, but it's one of the few newer technologies where I think it's a clear win.
Typescript's typing system is great and (for me) makes frontend development tolerable. It's annoying that it's optional. tsconfig and the related ecosystem is a disaster.
I still have hopes that Kotlinjs can fix the distribution size and the tooling around binding to js libraries.
This applies to much more than static typing as well. Anything built, can be built well or it can be built quickly.
Although I often have to make trade-offs for immediate gain at work, there is a big difference between the quality of my work over time vs the quality of other devs who default to immediate returns. I will say in their defense, they play a role in the team. But I wouldn't want to work on a team where avoiding upfront costs was the expectation rather than the exception.
This sounds like its just Typescript evangelizing. But while I love types, Typescript kind of sucks and sucks the fun out of writing Javascript. If I am going to add a compilation step to get types, I would rather write Scala or F# and compile to JS, then you also get rid of all of the warts of JS that Typescript doesn't get rid of.
And I definitely think less of you as you as a developer as you openly admit to dogmatic and myopic prejudice regarding the nuances of typing systems and the complexity of software development contexts.
Interesting. Would you have had the same response if GP stated:
"I've worked with my fair share of vanilla JS devs who refuse to acknowledge the benefit of testing. Most of them bemoan the upfront cost of testing everything without realizing the benefits of it. I absolutely think less of them as developers."
Again another binary dichotomy that doesn't neatly fit reality. I imagine the number of developers that would unilaterally dismiss all testing practices is very small.
I mean, I'm not going to say it to someone's face but I'll definitely be judging them. Most of those who have pushed back against typescript have done so for similarly dogmatic reasons or even straight ignorance (I dont want to learn something new), so I'm not sure who is really the winner or loser in this situation.
I absolutely realize the benefits of it, and I also recognize it doesn't cure all. Everything has trade-offs. There were plenty of amazingly complex and large projects written in vanilla JS before typescript ever existed, and somehow they aren't falling apart and crippled. It really depends on the developers involved more than the language or the dogma.
> I can see both side of the arguments on many topics, such as vim vs. emacs, tabs vs. spaces, and even much more controversial ones. Though in this case, the costs are so low compared to the benefits that I just don't understand why anyone would ever choose not to use types.
> I'd love to know what I'm missing, but until then: Strong typing is a hill I'm willing to die on.
I genuinely want to know what I'm missing. I also outlined in the post all of the arguments I usually see in favor of not having types and why I disagree with them.
I'm happy, willing, and excited to hear what I'm missing.
There's a specific scenario where I have more sympathy for weak typing, which is: I'm writing a small, focused utility by myself whose scope can never expand and whose code will never be collaborated on. I do have these, little shell scripts. I write them, they work, I move on and use it for 10 years. No types in there, fine whatever.
As soon as there's any complexity at all or another person involved that exception stops.
Note that weak typing and dynamic typing are orthogonal. I believe you're expressing sympathy for strong dynamic typing: types are checked an enforced dynamically by the runtime (such as Python raising an exception if you try to add a string and an int), but there's no static type analysis done at compile / load time. Python, JS, Ruby, etc. are strong dynamically typed languages.
Also, type weakness is more a property of the runtime than the language, but for those languages with specifications, the language specification usually also specifies a large amount of behavior for a compliant runtime.
The C runtime, will gladly (with UB nasal demon caveats) allow you to treat an int as a pointer. There are static type checks, but no hard limits on the type-confused nonsense it will attempt to execute. C is statically typed, but weakly typed.
Java, C#, etc. are strong statically typed. There are static checks at compile time, and the runtimes do their best (modulo escape hatches) to use dynamic runtime type checks to plug the gaps in the static type systems. (For any sufficiently complex sound/consistent type system in a Turing-complete language, as per Godel's incompleteness theorem and the halting problem, there will be some programs that cannot statically type-check but still will never encounter a type error, regardless of input. So, in practice, there will always be escape hatches in the type system to allow programs that are correct but not provable. Hopefully most of these escape hatches are backed by dynamic runtime checks.)
Of course, these categories are a partially-ordered continuum, not clear binary distinctions. For some pairs of languages, you can say one's statically enforced properties are a superset of the other or that one runtime enforces a strict superset of the other's dynamic checks. However, it's often a case that you can't say one language strictly offers stronger static type guarantees or one runtime strictly enforces stronger dynamic type restrictions.
I’m aware of the difference, I was contemplating bash in my comment. Some people construe it as strongly typed though it’s a little ambiguous, here’s another discussion about it https://news.ycombinator.com/item?id=12704050
You are the other person. Come back in six months needing to make a change, and it's almost like you're new to the code.
Now, in general, I agree with you. If it's a one-off (or if it's really never going to need maintenance), and if it's small enough that you don't need types while writing it, then sure, do whatever is easiest at the time. But "never going to need maintenance" often turns out to be a lie, and when that time comes, you may be happy for some types as signposts to give a hint of what you were thinking all those months or years ago.
I want a shell scripting language that allows a max line length of (for example) 128 characters and a max script length of (for example) 128 lines, with no `source` equivalent. If the script expands to not be a "small, focused" single-file script it should just fail to run at all. Just enough for small utilities, but unusable for giant monstrosities.
... and unable to call external programs or communicate with external programs via pipes / sockets / etc.?
Otherwise, if you're unable to source another script, you could just have another script return a normal result in addition to a JSON blob of the environment variable changes the caller should make, which is probably worse than just allowing sourcing.
Pipes allow arbitrarily complex networks of communicating sequential processes. In some cases, networks of tiny CSPs are cleaner, but without discipline, they can rapidly become worse than huge monoliths.
That doesn't prevent the problem of length limits encouraging users to break their programs into networks of smaller programs communicating over pipes. I'm sure that sort of thing can be done well, but it certainly takes discipline.
Particularly as programs start out simple and organically grow, once they start hitting length limits, they're going to start evolving into locally-distributed computation. Maybe you get something nice like composable small unix-like utilities. However, I suspect there's a large overlap between the set of developers who would do that well and the set of developers who would still keep things nice and modular within a single process if they didn't have size limits.
Hey, I hear you, and I commend you for your good faith position of "I want to hear both sides". I'm just not there anymore. The arguments put forth ("the cost is worth it, it makes up for itself", etc) are arguments I've had a million times and I just no longer will engage.
Oh, apologies, I misread your comment. I thought you were saying that you won't engage in this discussion because of my strong stance on the topic. I'm a big fan of "Strong beliefs, weakly held".
> I just don't understand why anyone would ever choose not to use types.
Is there a language out there that gives you that choice?
I expect you mean choose not to use strong static typing as per the original piece? Compatibility is a pretty good reason. I'd like to see Javascript die in the fiery pits of hell as much as the next guy, but its positioning means it is almost inevitable that some system will make it the only reasonable choice for you to choose if you want to build for that system.
Typescript doesn't help. It adds static typing, but not strong typing.
Static typing is already quite a help, as it makes whole categories of errors compile-time errors.
> I'd like to see Javascript die in the fiery pits of hell as much as the next guy
Part of the problem is that some of those "next guys" don't have the experience and/or vision to realize that it needs to die. Perhaps we should emulate Cato in our subsequent HN posts:
And furthermore, I consider it necessary that Javascript be replaced with WebAssembly so that we can use well-designed languages in the browser.
Python and PHP both have type hints that are natively ignored by the language if present, but can be checked if you have enough of them, if that's what you mean.
C++ has chosen a special place in the hellscape of language design by allowing you to silence the compiler using casts, where almost anything you do with the resulting object is undefined behaviour no diagnostic required.
Said UB is observationally indistinguishable from miscompilation under some toolchains under some optimisation controls.
It also has various bolt on weirdness like const doesn't mean the thing won't be changed by some other pointer so you can't constant propagate based on it, unless it's written on the global, at which point attempts to mutate it anyway may succeed under the usual UB challenges.
Maybe that's a "strong" type system, but you'd only define it like that if you started by taking C++ as axiomatically reasonable.
The only other use I'm familiar with where strong types are used to describe the equivalent of static types, but we've made clear distinction between the two here. What other definition is there?
Usually: A strong type system does not allow types to change after being established. A weak type system allows types to change. This is also described in the original article.
C++ has casts. I don't regard those as making it a "weak" type system. I regard it as "strong type system with escape hatches", which is not the same thing. A strong type system should not be a straightjacket. (Languages that try to make types bulletproof tend to get used less than languages that allow escape hatches, and rightly so. The language designer never knows what the union of all use cases will be. Good language designers know that the user may have a case the designer didn't think of, and allow some flexibility to hopefully handle it.)
My personal notion of a strong type system is one built around function type signatures.
In order to have that, you need static typing of variables and constants, like most statically typed imperative languages have. But you also need a method to specify required input types and expected output types of all functions.
In a purely functional language like Haskell where there are only functions, and functions are first class and can be both inputs to or outputs from other functions, then the entire operation of the program is encapsulated in its function type signatures.
The entire flow of data and logic through the program can be type-checked by the compiler, and function implementations checked against their type signatures.
Casts don't change the type of an object. They might create a new object of another type from the first one.
edit: I guess what you want to say is that implicit casts make a type system weak. But even there there is plenty of wiggle room: I think that everybody agree that implicitly converting the string "1" to an integer is bad (which is not allowed in C++). Narrowing conversions are arguably bad (they are sometimes allowed in C++), but some other implicit conversions are hard to argue against (int to long for example, or derived to base).
> A strong type system does not allow types to change after being established. A weak type system allows types to change.
For dynamic types, sure, that's a reasonable enough definition. But it doesn't really make sense for static types: they're attached to expressions in your source code, not runtime values.
no. c++ allows you to convert a value of one type into a value of another type (in a very limited number of cases) but you cannot change the type of an object.
I'm also a pretty die-hard type-system user, but I've been programming a lot of Lisp and FORTH lately. FORTH is just bad at safety period. Lisp, on the other hand, I can see why there really isn't a need for types. The macro system means you can create arbitrary "compile-time" safety checks which is, IMO, more powerful than just a type system. That being said, I would still love a strongly, statically typed Lisp, or at least one with a strong macro type system (before y'all mention it, I'm not a fan of typed Racket).
I'm more into Scheme than CL, but am aware of Coalton. My current lisp is Gerbil: https://cons.io which already has a type annotation system and will be enhancing it for the next major release (v19).
Well, every programming language must necessarily have types to some degree, but they aren't necessarily static or strong type systems. Most Lisps do not have a static type system and even fewer have a "strong" type system.
> Well, every programming language must necessarily have types to some degree
The vast majority yes, that's why I was a bit confused :D
There some languages that have no types. The only thing I can think of though is a POSIX shell minus arrays. (Edit: Assembly and Forth are two better examples)
> Most Lisps do not have a static type system
True
> and even fewer have a "strong" type system.
Common Lisp, Emacs Lisp, Scheme, Hylang, Clojure, and Racket all feature strong typing. I'm curious where you have found this trove of weakly typed Lisp dialects
Lisps are not statically typed typically and "strongly" typed is poorly defined. I would not consider most Lisp's type systems to be strong or particularly expressive, but they don't need to be because it would hurt the main benefit to the language: meta-programming. I don't think a "strong" vs "weak" type system argument is particularly valuable here, yes most prevent you from inadvertently changing how a particular series of bytes is interpreted, but their aggregates do not have any type identity besides their own typically. This means you cannot typically express and enforce an aggregate's covariance or contravariance properties. There may be ways to do so, but most do not provide utilities for it.
In Lisps, and other dynamic languages, two or more objects that are not related by inheritance can be substitutable. This is very useful.
That makes inheritance-based covariance and contravariance largely moot.
You have substitutability-based covariance and contravariance (you can't get away from those) but they cannot be subdued declaratively.
E.g. if you're passing a callback function somewhere, which you know will pass widget objects to the callback, it's okay to use a function that was written to handle gadget objects, if widget objects are designed to substitute for gadget objects.
That's contravariance of substitutability.
Substitutability is the only thing that matters in the end. Declared inheritance doesn't ipso facto guarantee substitutability, and therefore declared covariance or contravariance, which are inheritance based, do not guarantee actual substitutability-based covariance or contravariance.
> I don't think a "strong" vs "weak" type system argument is particularly valuable here, yes most prevent you from inadvertently changing how a particular series of bytes is interpreted [...]
I think that interpretation of the "strong" vs "weak" scale is a valuable one within the context of the blog post. The post is at least partially about how type systems can help the programmer by making them aware of certain kinds of errors (it is also about when: static vs dynamic).
My understanding of the terms covariance and contravariance are a bit shaky. Could you provide an example in another language that you think cannot be expressed using the provided utilities of most Lisp's?
You also mentioned that you don't think most Lisp's have "expressive" type systems. What do you mean by that? When I think of a type system as being expressive, I think of it as having explicit rather than implicit types, which is unrelated to the issue of strongly vs weakly typed and static vs dynamic types. Do you mean more like how you can describe / constrain the relationships between types in certain strongly typed languages?
> I think that interpretation of the "strong" vs "weak" scale is a valuable one within the context of the blog post. The post is at least partially about how type systems can help the programmer by making them aware of certain kinds of errors (it is also about when: static vs dynamic).
Sure, agree. That's also the real point: type systems primarily exist to make semantically impossible computations unrepresentable in the language (at least, without some extra song and dance). To this end, Lisps have rather lackluster type systems, but they don't try to encode much of the languages semantics into a type system.
> My understanding of the terms covariance and contravariance are a bit shaky. Could you provide an example in another language that you think cannot be expressed using the provided utilities of most Lisp's?
I'm actually mostly concerned with type invariants (ex. List[int]), which don't really have much for representation in Lisps from what I've seen. Further, see above about using types to make compile-time assertions/checks about the behavior at runtime.
> You also mentioned that you don't think most Lisp's have "expressive" type systems. What do you mean by that? When I think of a type system as being expressive, I think of it as having explicit rather than implicit types, which is unrelated to the issue of strongly vs weakly typed and static vs dynamic types. Do you mean more like how you can describe / constrain the relationships between types in certain strongly typed languages?
"Expressive" is a nothing word that doesn't have concrete meaning in-context, similar to "strong" type system. That being said, I would say Rust, OCaml, and TypeScript have expressive type systems: the behavior of the language is largely encoded as types. The implicit vs explicit nature of types is not super consequential IMO, it has more to do with how you primarily represent semantic meaning. In lisp, it's symbols. In Rust, it's traits, enums, and structs (+ the affine types, but that's not relevant here).
Why dynamic typing is required for metaprogramming? D (and rust and c++ to a significantly lesser extent) for example has extremely strong metaprogramming capabilities while being statically typed.
I didn't say it was required, I said it hurts it in Lisps case. One of Lisps strongest features is extreme flexibility and homoiconicity. Strict typing would make it clunkier, though maybe it's worth it for some scenarios.
wait, are you saying that any sufficiently complicated lisp program contains an ad hoc, informally-specified, bug-ridden, slow implementation of a static type system?
Consider reading/watching Rich Hickey's talk Effective Programs [0][1] and Maybe Not [2][3]
In [0] in particular there're slides 22/23, here is part of the transcript but makes more sense with the slides on:
> And you can call them problems, and I'm going to call them the problems of programming. And I've ordered them here [...] I've ordered them here in terms of severity. And severity manifests itself in a couple of ways. Most important, cost. What's the cost of getting this wrong? At the very top you have the ___domain complexity, about which you could do nothing. This is just the world. It's as complex as it is.
>
> But the very next level is the where we start programming, right? We look at the world and say, "I've got an idea about how this is and how it's supposed to be and how, you know, my program can be effective about addressing it". And the problem is, if you don't have a good idea about how the world is, or you can't map that well to a solution, everything downstream from that is going to fail. There's no surviving this misconception problem. And the cost of dealing with misconceptions is incredibly high.
>
>So this is 10x, a full order of magnitude reduction in (?) severity before we get to the set of problems I think are more in the ___domain of what programming languages can help with, right? And because you can read these they'll all going to come up in a second as I go through each one on some slide so I'm not going to read them all out right now. But importantly there's another break where we get to trivialisms of problems in programming. Like typos and just being inconsistent, like, you thought you're going to have a list of strings and you put a number in there. That happens, you know, people make those kinds of mistakes, they're pretty inexpensive.
In my experience whenever you put effort into making a post like this, outlining the common arguments you hear and your attempt at refuting them, people don't really pay attention and will just repeat the arguments you already addressed, ignoring your refutations.
Youre missing the perspective of a business owner and operator that has to hire many people with diverse patterns of thoughts and beliefs who will most likely work for their organization for less than 24 months.
As far as I'm aware, the generally accepted definition of strong typing is: some object in memory has a type, and the compiler (or interpreter) will not let you treat it as another type without some kind of explicit instruction to do so. This is in contrast to weak typing, where the compiler/interpreter will happily try to treat a Bar object as a Foo object if you do some Foo operation on it.
Memory safety is definitely not equivalent. Memory safety is more like not going past the end of an array, not doing use after free, etc. I agree that what I said is pretty much also the definition of type safety. But in my experience it is also the generally accepted definition of strong typing.
I think you mean "involves non-static typing"? If so, Python. You can't (for example) do something like '1 + "2"', because those types aren't compatible. It's dynamically, but strongly, typed.
Weak typing has its uses in one use case. Transition between systems. Even then you usually want to know what the type is (unless you really do not care about validation errors).
The reality is I never get to choose. I prefer strong typing. But the projects I am on that ship sailed long ago 2 developers back who used this project as a resume builder.
if you get the static type system wrong once, there's no going back
Take for example, Rust: mut, Send, Copy, Drop, Debug, dyn have all their warts because they are special and you can't fix them because you'll break previous code.
In a gradual typing language you could potentially bolt-on your own type system as a package where it just runs the type checker if you want one. It just so happens people who favor these gradual typing systems don't do a good job of creating those typing systems because they are not that into types in the first place.
But in theory, this kind of a system would let you not worry about types when prototyping the system, then put some bounds later based on your requirements.
This is not about type annotations, it’s about type systems. You can write Haskell programs without any type annotations. Your program will type check and compile just fine. The value comes from the checks (as well as the inference, documentation, optimization, and everything else a modern type system gives you), which you don’t have to write yourself, you get them for free.
People who say they prefer dynamic languages are really just saying ‘no’ to all these free benefits.
Even though I usually fall more on the "types good" side of the discussion, the advantages you listed are not free at all. They come at the cost of at least:
- having a compile step in between writing and running your code. This drastically slows down the feedback loop of development. The more your compiler has to check for you, the slower it gets.
- being able to run your program in a half-broken state. This may not seem like much of an advantage, but sometimes it is good to just be able to run a broken piece of code to see how it crashes. This is especially important when learning to program, but is still very beneficial when learning a new language or framework, or sometimes for debugging.
- As someone who has contributed to the main Haskell compiler, I can definitely confirm that a more complicated type system can slow down development of the language itself too. It takes significantly more effort to grok all the possible interactions as the codebase of the compiler grows.
Don't get me wrong, I think encoding and enforcing program properties with types is a great idea that will grow further in the future. But the dynamic languages gained popularity for good reasons, and some of those reasons are still valid today.
If your language is wholly type inferred, and the type system is expressive enough to deal with whatever one wishes to write, it is indeed "free". If you ignore compile time performance, run time performance, and that you're writing in something between Idris and vapourware.
Otherwise the cost is merely being unable to write anything that the type checker does not understand, and however long your compiler takes to do the checks on what it does understand.
Also a fair chance the whole program must type check before you can see the results of changing a subset. In the worst case you get to hunt down all the unused variables before it'll run the test suite.
I prefer dynamic languages. That makes me a heathen on these boards, to be ignored or chastised for my stupidity. Regardless, those benefits do not come for free.
type ":t" in ghci for many common Haskell types (functions are also types fyi) and tell me if you have the faintest idea what you are reading
lots of comments here praising Haskell and Rust frankly seem to be coming from people with a superficial understanding or limited exposure
I've had rustc complain about type deductions that were over a line long...that's just one inferred type...you can easily paint yourself into a corner with a type system with no exit other than trying to cajole the right definition out of the compiler and then you copy-paste it and pray
If you are proponent of strong typing, you need something that is effectively a theorem prover. I.e when you define a type, you define the scope of the data it can hold, operations on that data, and the resultant types of those operations. That way, when you code compiles, it is by definition "correct".
When you accept anything less then that, you are basically making a statement that you are willing to forgo some of that correctness for convenience, which is fine, but that means that no language out there is really good or bad.
Theorem provers' type systems are not necessarily stronger than SML's or Haskell's; they are more expressive, which is a separate thing entirely.
That said, your position is also just weird to me. Yes, theorem provers have nice type systems that are wonderfully expressive. The trade-off is that some common programming patterns become difficult to use or are even impossible.
When it comes to everyday programming, I don't think theorem provers are at a point where they are particularly useful. Not everything needs to be proved formally, and I don't think this position is at odds with the belief that static type systems are generally "better" than dynamic ones.
> The trade-off is that some common programming patterns become difficult to use or are even impossible.
Not really. Strong and expressive typing at its core is simply creating data packaging containers that have defined operations on them. It says nothing about logic. You may be referring to the functional programming aspect that comes with strong typed languages, which is related but not the same as strong typing.
The point is that typing is just a tool that a programer can use, whether its built into the language or ran statically like MyPy. You can take a piece of code in C, and write a test suite on input and output of that piece of code, and accomplish much of the same thing that typing accomplishes. However, in the case of strict+explicit typing, the idea is that you wouldn't need tests in the first place, because your code would be correct by nature of compilation.
Some theorem provers, such as Coq, are not Turing-complete. This means you cannot write some programs, and in particular you cannot write infinite loops in Coq. Infinite loops are a common pattern (e.g., a REPL or a GUI display waiting for input).
This is a trade-off, as I said. You gain the expressive type system, but lose the ability to implement certain programming patterns. It has nothing to do with the strength of the type system.
---
> whether its built into the language or ran statically like MyPy.
All (true) type-checking is static, so MyPy "running statically" is not a noteworthy feature to distinguish it from other type checkers. I guess this point may seem trivial to some, but the broader context of this conversation involves conflation of the terms "static type system" and "strong type system", so your misuse of the word "statically" here seems worth pointing out.
That said, whether you use an external tool to check your types or the type-checker is built into the compiler is irrelevant, and I'm not sure why you brought it up at all.
---
> You can take a piece of code in C, and write a test suite on input and output of that piece of code, and accomplish much of the same thing that typing accomplishes.
Depending on the perspective, this is factually incorrect.
Type-checking is an ahead-of-time operation that guarantees the absence of certain classes of errors at run-time. Writing tests to check for the absence of such errors is not equivalent, because you have not proved anything; you merely gain confidence. They are semantically distinct, even if you write many tests to gain a lot of confidence.
>Some theorem provers, such as Coq, are not Turing-complete. This means you cannot write some programs, and in particular you cannot write infinite loops in Coq.
You are talking about languages, Im talking about the concept. The modern theorem provers aren't up to the task. Due to Rices theorem, you cannot "fully prove" a program, so a language that does this couldn't even exist. The strict typing however can be applied to subsets of the programming space, namely the data processing pipeline, whereas higher level stuff like the actual server code that does have an infinite loop to listen to requests can be written in whatever.
The point is that no language recommended for strong type safety today is anywhere fully complete to include the rigor of something like a theorem prover, and anything less then that is basically your own opinion on what is "good enough".
>Type-checking is an ahead-of-time operation that guarantees the absence of certain classes of errors at run-time.
Run time errors are no different than compile time errors as far as testing is concerned. Its not like the computer blows up when you have a seg fault. And you absolutely can prove what you need for operation.
Say your input to your code is an HTTP request of length x. And output is some data processing on that request. You can write a test suite that is basically like this
1. Ensure that code returns a well defined error for x values outside of given range.
2. For all valid ranges in x, test all possible values of every byte in that range, and ensure correct behaviour.
While overkill, this will absolutely exercise every single piece of your code and prove correctness. You can also couple this with checking things like memory access
SML is good enough for theorem proving. That's what it was written for. Though you probably want convenience libraries on top, maybe the one called HOL.
> At this point it's beyond a hill I'm willing do die on. I'm not really interested in discussing it. If you don't get it I probably don't want to talk to you about it, I might even think less of you as a software developer.
That's a pretty strong take! I don't think there are many things that I feel similarly about... maybe if someone suggested that they don't need test environments and can just deploy changes to prod without testing or CI/CD and just see what happens, when it'd be my employment on the line, but even that's a pretty contrived and out there example.
> The amount of pre-existing respect for someone I'd need to have before I engage in a good-faith discussion on "are types good" is pretty high.
My problem is that not all type systems and the way you use them are equal.
When working with back end code, I really like .NET or even Java having a type system there for me. I know that people suggest that they have their own shortcomings (type erasure, NPEs, no multiple inheritance, even smaller things like C# enums not supporting methods) and that there are better options out there, but generally you can turn off the part of your brain that'd worry about the language too much and just deal with the ___domain problem at hand. Something like JetBrains are also excellent, because with the type system suddenly the tool also can reason about the language constructs you're using and give you all sorts of good suggestions and refactoring options.
Whereas with something like TypeScript in combination with React, there are times where you fight the type system instead. That's just the impression that I got working on a few projects for a while, in comparison to React with JS (perhaps the code was also a bit too clever), while with Angular it felt more coherent to me (despite Angular being more complex otherwise and not really my first choice). In the end, I gravitate towards Vue with JS for my personal stuff, but it's not like you can just say no to TypeScript when you need to maintain something long term.
I can't actually remember who said that they ditched TypeScript for similar reasons, but the argument was basically that a non-insignificant part of their codebase was there just to satisfy the type system. TypeScript does what it's supposed to... but it feels like it could be easier.
> Some type systems suck so badly that I can see why people would be tempted to believe that all type systems suck.
But that's my point: people say that exact thing about Java and .NET, while I find them usable. Meanwhile TypeScript has cool stuff like union types and other stuff to the point where you can get pretty clever with it (https://codegolf.stackexchange.com/questions/237784/tips-for...), which many would describe as the type system being objectively better, yet it's also more difficult for me to use.
In my mind, a good type system would let you do both basic stuff easily without too much work (to make sure that refactoring doesn't make you shoot yourself in the foot) and also encourage you to write the simplest code that you can get away with, while allowing you to get clever in the select few places where that is actually needed.
Which is funny, because adjacent to that, Java and .NET (web) frameworks can be a masterclass in incidental complexity, even though for me the type systems don't get in the way too much.
Edit: actually, I think I'll migrate a JS project to TS, this time in Vue. Perhaps Vue 3 will be a pleasant experience and if it won't, then I'll have a concrete list of things that caused me to feel this way.
I'm pretty agnostic about the whole thing. The topic has been done to death already. Types are great, but some ways of handling types can get in the way sometimes, and the excessive type declarations in older Java was just painful. Dynamic types can work fine too, but they work better in smaller projects than in bigger ones. Types do give you a lot more to hold onto, even if they're only compile-time types like Typescript. In large, complex projects where you're juggling a lot of different types, you want to know what you've got in your hands.
That's pretty much my take. I don't hate anyone for having different ideas about it. But if you're going to use Typescript, don't use 'any'. At all. Unless you really don't know and the next step is figuring out what type you've got.
>maybe if someone suggested that they don't need test environments and can just deploy changes to prod without testing or CI/CD and just see what happens
That's actually not hard, you need to be able to run in dry mode, and run the same in an existing instance (in parallel), then compare the results. If you are happy you can continue with the roll up, disabling the dry mode.
I think this is especially true for line of business applications that have the following qualities:
1. It's simple. Let's not lie, let's not pretend. It's CRUD. You can put in K8S, you can add AI, it can be behind an API Gateway, you can make it all Event Driven, you can use CQRS for every entity because you really, really want to feel clever. But it's still CRUD.
2. Other people are working on it, many other people.
3. People from other companies are interfacing with it.
So knowing what to expect trumps everything. Types help with that. They help a lot.
I'm all for healthy discussions, but I think it's fair to say "I've had this discussion enough times, unless you're really bringing something new to the table, I'm opting out".
Is it really a healthy discussion? At least with respect to JS vs TS (which is the most common argument as it's a choice every front end team has to make), it's blogs saying "fuck types, I like JS only." I'm not going to step in and tell another team what to use but whining about the difficulty of static typing is just not a productive conversation. I've been converted by the top level comment; I'm not longer interested in entertaining the discussion.
I find it interesting that people in this thread seem to have absolute certainty that "types good" is true, while to me those two words together are largely non-sensical, just like "bytes good" wouldn't make much sense to debate.
After you repeatedly waste a bunch of time debugging things that static typing would have caught on the first compile, and you start developing a "types good" attitude. If you just do simple React development, you might not ever run into the problem. But once your programs get to a couple thousand lines, you start occasionally forgetting what your function takes and passing in the wrong things, which then get passed around for a while and throw an exception long after the problem happened. This results in very unfun print-debugging, particularly in a recursive descent parser or an algorithm that processes a lot of data.
>But once your programs get to a couple thousand lines, you start occasionally forgetting what your function takes and passing in the wrong things, which then get passed around for a while and throw an exception long after the problem happened.
I've been writing code for 40 years, I can't estimate how many loc I've written, but likely over a million. One of my personal projects is currently over 65k loc vanilla js, and I never once had a problem with not knowing what type a function took. If you're so bad at naming things and knowing what a function does, I guess maybe types can help you. But not everyone needs it.
I'll take the time to reply to both of you, because you seem to have opposing views.
I think such a debate is largely unproductive. Like anything else, types have value (ha) and successfully capitalizing on that value depends on the context of the project, which includes things like developer experience, tooling, project complexity, requirements, deadlines etc.
The only productive outcome of these debates is that each developer gets to slowly and frustratingly build a list of pros and cons as they go through the arguments presented by either side debating this topic. In addition, developers who completely disregard either the cons or the pros are necessarily making subjective decisions with incomplete data, and the project gets to pay the price. Just because the developer is personally OK with all of their projects paying that price, doesn't mean it's the best decision for a project.
My experience has been that when starting out, projects get the most value out of not having types, and as they grow in scope and size, and the cons of not having types start creeping up, that's the point when gradually transitioning the code to being strongly typed allows the project to maintain its velocity _and_ quality.
I don't use dynamically typed languages because they're dynamically typed. I use them for other reasons and they happen to be dynamically typed.
I'm not going to argue that these features are only possible because of dynamic typing, but regardless, statically typed languages tend not to have them.
Elixir may get "some form" of typing, but it likely won't be traditional static typing.
STM, REPL, and data driven nature of Clojure. I know most people don't use STM in Clojure, but I find it to be a killer feature for some projects. Combined with pervasive immutability and it enables concurrent designs that are very difficult to pull off in other languages.
For Elixir, the concurrency model and supervisor trees. It's a perfect fit for some problems, and in general as a small company, the projects we use Elixir on greatly simplifies our production environment which is always a win.
Most of my focus when designing a project is to enable people with less experience than me to contribute in a bug-free fashion in the face of concurrency and parallelism. Sometimes that involves picking a funny language, sometimes it doesn't.
This is the sort of position that is baffling to me. Software engineers claim to be very logical, but this boils down to a preference being stated as some kind of objective truth. Show the evidence (not anecdotes) that static typing gives benefits that are worth the cost. My anecdotal evidence is that I rarely come across problems in production code that would be solved by static typing and people make the same sorts of logic mistakes in languages like Java and Typescript.
Null reference errors are examples of errors that can be caught before runtime by a static type system. The fact that static type systems like Java don't catch this is evidence that static vs. dynamic is not a binary - some static type systems are better than others.
Well, getting this personal about a purely emotional preference might make people think less of a person that does that, with significantly better justification.
To be clear: people have tried to show the long-claimed safety benefits for a long time, and they just refuse to appear.
Unfortunately this attitude is not profitable, and also smacks of someone who works on their own island and costs a business more than they add value given their inability to work as a team. This attitude might find a way to be profitable when held by a university professor, though that does not mean its desirable.
This is not a "work" attitude. I would never impose my opinions on a team. I've worked professionally in many languages with a variety of type systems. In virtually all cases I have reviewed very positive feedback throughout my career, in particular in terms of my ability to work well with others, to mentor, etc.
How often do you write bash scripts or do you always write all automation in some other language? Just curious how much your conviction influences your behavior
I write code without static types all the time. Daily probably. I am "pro types" in a hand wavy way. My conviction is to no longer engaging in the debate.
Speaking for myself, I never write bash scripts if I can help it (though sometimes I can't help it). That isn't so much because of typing, though... it's because bash is a fucking horrible language that should have died 30 years ago. The fact that there's no such thing as types in bash is just one of many reasons why it sucks to use.
I'm currently responsible for a very large system built in raw javascript where function definitions like this one in the article:
function birthdayGreeting1(...params)
...are the religion.
It's awful. I hold the people responsible in very little regard.
Mostly, it boils down to selfishness and "laziness". (Although I try to be very careful about that term, because I do not believe in it as generally used. I use it here to define: an unwillingness to plan ahead and to have to learn something new. Not applied as a global personality trait but only used in context.)
First way is to use a strongly typed language, think about the data, and design your code in the appropriate way. You code it up, go through the loop of compiling and fixing errors, and get your code to run.
The second way is to write your code in Python, without worrying about strong types. You complete the code quite a bit faster, but since you also want your code to be correct, you spend time writing an end to end test suit for your code.
The second approach is not only faster (since you are writing tests in both cases), its overall better. Spending time writing tests allows you to essentially validate things that modern mainstream strongly typed languages can't catch at compile time (for example, what happens when the input is unicode strings?). It also forces you to think about end to end behavior and making sure that is correct, rather than just the behavior within your code.
Strong typing is a simply hand-holding tool for programmers. If you cannot write correct code without it, you are on a fast track to being replaced by AI eventually. The future of programming is not going to be designing data structures and types, its going to be using English to generate large chunks of code in most likely Python, and then tweak fine details in those.
Call me sceptical, but I don’t buy it. What makes you certain that’s the (one) future… Also programming is a super broad field, and more then a field is almost more like a medium then a field.
I honestly don’t know if we’ll still program by hand or not, but I do look skeptical at people who are very certain we won’t. Don’t think that’s the first time in history people make that prediction…
Compilation at its core is a translation problem. Right now, it would be fairly trivial to train an LLM to essentially be a compiler. And, you can already prompt LLMs on generating code for a wide array of things.
As far as adoption, there is a reason why dynamic type languages that feature a lot more natural syntax (Node, Python) are used WAY more than others.
I think this is completely false. It completely breaks down as soon as you get into your first refactoring that is a bit larger than completely trivial. Typically the mess one ends up is one or more of the following. (1) Nothing is refactored because people do not dare to do it. (2) Not just some but most of the uncommon code paths are broken. (3) Tools that are used to interact with a running application are regularly in a broken state. You want to prevent these problems with 100% test coverage. As such this may be a laudable goal on occasion but I do notice that the code described in (2) and (3) are highly non-trivial to cover 100%. I also have to wonder what mythical developer is does not have the discipline to use static typing but does have the discipline to attain 100% code coverage. I first want to see such a developer before I believe in his/her existence.
>It completely breaks down as soon as you get into your first refactoring that is a bit larger than completely trivial.
This is, in fact, MUCH easier to do with a strong test suite that only cares about input and output rather than internals.
The most common approach to starting a refactoring project is write or enhance a test suite to the point where there is no undefined behavior, either the program works or handles the appropriate errors.
You don't need to aim for 100% test coverage either, you just need your tests to cover all possible inputs (including fuzzing).
You can, but the reason you do this in practice is because the static typing languages are often not sufficient or strong enough. And if you are writing tests, you may as well do everything through testing and focus on rapid dev rather than worrying about types.
I actually like this approach. I believe in mentoring. I believe in building bridges. But if we're on opposite sides of the Grand Canyon, it's not worth my time.
You're confusing static typing with the existence of a type system. As an example, Julia has a rich type system, but the language is dynamically typed.
I think the conversation becomes a lot easier once you recognize that there is no such thing as untyped data.
There's only explicit typing and implicit typing.
So the only real argument you're having with someone is when writing a piece of code, do they want the caller of the code or the input of the code's data type to be known or they want the data type to be a mystery to be figured out, occasionally in production when shit hits the fan.
Also on the “types good” side of the fence. HOWEVER, I also believe that mainstream typed languages cannot represent some situations very well. Often the only attempts to do so are in some weird Haskell extension and it feels rather… inelegant. Bottom line: types are better; but our current type-systems are not the final word on types.
There's a temporal component to the argument. Strong typing in the 1990s wasn't very good. Dogma at the time resulted in very rigid designs. The verbosity of it all was staggering. Incorrectly-used Hungarian notation ran rampant and made everything hard to read while still not helping anything [1]. The costs were a lot higher than it is today and the benefits a lot less.
Nowadays, the static typing is much nicer, with both higher benefits and lower costs, and it makes the costs/benefits analysis much more likely to come out in favor of static types.
In the late 1990s when I was cutting my teeth, I did a lot of Python, and I was almost 100% dynamic language until ~2015. In hindsight, I might do the same again even if thrust back in time. There just isn't a great static option back then. (I'm not saying 2015 is the year it became practical, I'm saying that's when I finally moved into static languages. 2010-2015 or so I was in Erlang, doing things that most other languages couldn't do at the time, so that forced me into a dynamic language. C# was looking pretty good in that time frame too, it just wouldn't have run the systems I had on anything like the resources I had at the time. It could probably easily do it in 2023 though.)
Now I even prototype in static systems, and it's a better experience than prototyping in Python was. Like, by quite a lot, honestly. In the end, I don't find it that much of an impediment to make sure that if I want to call a method on a thing, that the method actually exists.
Dynamic typing will never disappear; there's a certain small size of task for which it'll always be more advantageous than static typing, and while said tasks may be small, there's a lot more of them than there are large tasks, so it's a completely valid and sizable niche. But I do think over the next 10-20 years we're going to see the "scripting" languages return back to "scripting" and away from "systems".
I think that in the end, the dynamic scripting languages being used for large tasks will be seen as a reaction to a misdiagnosis of the problems in the 1990s. The code was atrocious in the 1990s not because it was statically typed, and therefore the solution is to go dynamically typed. The code was atrocious in the 1990s because it was poorly statically typed, and the solution was to get better. That said, "getting better" did take a long time, and for many legitimate reasons.
(Much ink is spilled on the so-called rapid pace of technological innovation in our industry, but programming languages move on decadal scales. Programming languages still have only barely grappled with a multicore world, and haven't grappled with a heterogenous computing world at all (GPUs on one end, efficiency cores on the other). Things are not always in as much motion as we fancy.)
[1]: Particularly, Hungarian notation is supposed to supplement the type, not just reiterate it. If you're in a language where you can't easily declare "a width is an int", then Hungarian notation suggests calling a width variable something like "wdthDialog", so that you stand a chance of noticing that you passed a "hghtDialog" in the wrong place. But the way it was used a lot of the time is you got "u16Width" instead, where u16 meant unsigned 16 bit int... but that's already in the type. Using it that way just adds an extra layer of hierglyphicness to the already ugly code. One of the several innovations that made static typing languages more feasible is that in most languages designed in the last couple of decades, you can declare something like this with something like "type Width int", and then you don't need to label any variables with it at all, the compiler enforces it.
I'm with you, especially in my professional life. That said, I think Clojure is interesting and Rich Hickey has some good talks in support of dynamic typing.
Strong static typing is useful in the environment where the code is not adequately tested. That's because tests adequate to make the code bulletproof will also detect the problems strong static typing could detect, rendering SST superfluous.
So, how often is poorly tested code out there? From the popularity of strong static typing, it must be ubiquitous.
Strong types aren't just a correctness guarantee, they also help to discover structure and interfaces that previously were implicit.
If a developer can jump into an unknown part of a codebase and quickly see that following a certain structure will automatically make their code work for them without needing to read all the code first and double checking if it's just a random convention versus a strict interface so they don't reinvent the wheel or build code that doesn't fit in with the existing structure, then that's worth a lot and something you cannot simply cover with tests.
That is a very original take, but I think you have it reversed. Typing removes the possibility of _some_ errors. Those are tests you do not _need_ to write (you can still write them if you want), thus having more time and attention to write tests that matter. Typing is a baseline, and anything over that baseline you can deal with with tests.
It is like the things you already trust when you code. Let's say, that the file system works, that the network stack of your OS works, or that the cpu works. You can rely on them to use your time for the things that matter. This is a much better model than simply testing everything yourself since, as the tests number increase, any change to the codebase involves more and more test changes.
> Strong static typing is useful in the environment where the code is not adequately tested. That's because tests adequate to make the code bulletproof will also detect the problems strong static typing could detect, rendering SST superfluous.
Your logic is the same as follows: seat belts are useful in an environment where drivers are not driving adequately. That's because adequate drivers will drive in a way to avoid any danger that the seat belt would prevent, rendering seat belts superfluous.
Ultimately, the problem is adequate drivers (as in described above) or bulletproof tests do not exist, they are just a concept. A test can prove the presence of an error, not the lack of errors, and the argument only works in absolutes.
You seem to be thinking that testing for type errors is something additional to general testing. It's not. With sufficiently strong logic testing, the testing for type errors comes along for free.
The analogy with driving doesn't make much sense. After all, accidents sometimes happen without the driver being to blame, and the marginal cost of putting on a seat belt is very low.
The point of static typing is that the compiler does checks before your program even starts. And that avoids to write lot of coverage tests that are needed (but usually just ignored) with languages that massively use loosely typed data containers (variables).
Sure, if you're going to be in an environment where the code is going to be bad because of inadequate testing (and I admit this is quite common, due to the cost of the testing needed), static typing lets it be somewhat less bad. It's just important to understand it's a band-aid, not a panacea.
Static typing, as done in mainstream languages, doesn't show your program is correct. Is you compiler written in a mainstream language correct just because it compiles? Of course not. Don't point to academic curiosities with dependent types if they aren't actually used. Extensive testing, on the other hand, is used in the "real world" for assurance.
Every code out there on the real world is full of baindaids and duct tape. Yet, the actual amount varies a lot, as does the centrality of the patched things.
Integration tests are different, but I tend to believe that a strong enough type system should be able to negate the need for unit tests completely.
If anyone can think of something a unit test could test for that an arbitrarily* complex/strong type system couldn't I'd be interested to hear it. It's possible, I just can't think of any.
When I say adequately tested, I mean much more than unit tests. Unit tests aren't going to give you bulletproof code. They don't detect bugs that arise from interactions at a higher level.
An arbitrarily complex type system, with dependent types, could detect any bug, since it's formally equivalent to requiring the program be proved correct. But using such a type system is so onerous that I don't know anyone who realistically does it in production.
If you wrote such types, you're basically giving a formal specification of what the program is supposed to do. That could then be used, and likely much more easily, for high volume property-based testing.
Unit tests and types solve somewhat different problems. You can force a unit test to check everything a type system does with effort (mostly by throwing wrong types at your API and verifying you get an error), but I'm not sure how you would use the type system to enforce sort() actually sorts. Sure sort can take a listOfFoo and return a sortedListOfFoo - but that doesn't verify sortedListOfFoo is actually sorted - and in many cases I want an API that is find with listOfFoo but should handle sortedListOfFoo as well.
With dependent types you can encode any proposition in first-order logic (?). You can indeed define an array type that is necessarily sorted. It won't be pretty but it's possible.
I like type systems as much as anyone but I can't see how your type system will check that sin(pi) == 0
if you have a type system expressive enough to do things like that, you've basically got another turing-complete layer on top of your existing language, which is itself ... dynamically typed.
> Strong static typing is useful in the environment where the code is not adequately tested. That's because tests adequate to make the code bulletproof will also detect the problems strong static typing could detect, rendering SST superfluous.
Ok, but it's cheaper to not have to write those tests than it is to write those tests.
Yes. It's cheaper to have a lower assurance your program is correct than to have a higher assurance. The claim I'm making is that at the high end, the extra assurance from strong static typing becomes minimal.
Yeah, big piles of untyped Django spaghetti need a behemoth suite of integration tests in order to remotely approach the sort of runtime guarantees that typed languages give you for free.
When you use a typed language, you can use your integration tests to actually verify business logic, exception handling, etc. instead of having to write a dozen test cases to make sure that doThing(table, index,*kwargs) doesn't blow up when 'table' is a list or 'index' is bytes...
If you adequately test your code for logic bugs, you test the type correctness for free. So, yeah, writing those logic tests is hard. But at least they are finding bugs that static type checking never will (absent the unrealistic scenario of proving your program correct via a type system.)
(It won't find latent bugs that can't currently be exercised, so the testing can't be one and done.)
you can't ensure that you hit every possible case in your test suite. type errors can still slip through in production, just like logic bugs do. it happens all the time in real-life code. that "adequately" is a no-true-scotsman.
if you use a static type system you can guarantee there will be no type errors at runtime. why on earth wouldn't you choose that? you can still write logic tests!
and when you leverage the type system to make illegal states unrepresentable, you can make certain classes of logic error impossible as well. some of your tests become tautologies and you can delete them.
You are assuming type errors are caught by tests explicitly written to detect them, as opposed to being found by other kinds of tests. For example, property based tests, with automated generation of inputs to a program or module, don't explicitly test for type errors, but are still (in my experience) quite good at hunting them out.
What testing cannot find are latent errors not exercised by the program. Do I care about these, though? Arguably these would be found by testing internal interfaces and elimination of code not reachable by tests.
The general argument I am trying to make is that the marginal value obtained by strong static typing declines as testing increases, and that in the limit goes to zero. If a program is adequately tested, is it still worth doing? This is not clear to me, and the arguments given here have not convincingly demonstrated that it is worth it.
Also: if you find yourself in a situation where strong static typing seems useful, you should be alarmed. It means you aren't testing your code very thoroughly.
>You are assuming type errors are caught by tests explicitly written to detect them, as opposed to being found by other kinds of tests.
I am not assuming that.
honest question: what language do you have in mind when you think "static types"? your perspective is so starkly different to mine, you seem to be operating under totally different assumptions about what types can and can't do.
I mean this sentence:
>Also: if you find yourself in a situation where strong static typing seems useful, you should be alarmed. It means you aren't testing your code very thoroughly.
is just baffling to me. it's so self-evidently absurd that I can't even argue against it. what is there even to say?
have you even used a modern statically typed language, with type inference, generics, null safety, algebraic data types, pattern matching, etc? I cannot imagine trying to maintain a big codebase without them. they don't slow me down, they speed me up. they aren't just about catching trivial int-instead-of-string bugs, they are are core tool for modelling data and business logic. they let me define problems out of existence (see e.g. this series of posts https://fsharpforfunandprofit.com/posts/designing-with-types... for an introduction).
and then there's rust and newer-generation languages with borrow checkers and affine/linear types, ruling out entire classes of memory and concurrency bugs ... your test suite cannot rule out data races, but rustc sure can.
you are making the same arguments people were making in the 2000s when most static languages sucked because they didn't have these features. I don't want to go back to a time before sum types.
>What testing cannot find are latent errors not exercised by the program. Do I care about these, though?
you should, because in production your program must endure orders of magnitude more variety in inputs, uptime, and runtime conditions than the test suite can exercise. it can and will get into weird states you didn't anticipate. yes, you can fuzz, yes you can property test, I know all about that. those things are good. but I don't get why you wouldn't also use a static type system to provably rule out classes of problem across all possible code paths. why settle for less?
"you should, because in production your program must endure orders of magnitude more variety in inputs, uptime, and runtime conditions than the test suite can exercise. it can and will get into weird states you didn't anticipate. yes, you can fuzz, yes you can property test, I know all about that. those things are good. but I don't get why you wouldn't also use a static type system to provably rule out classes of problem across all possible code paths. why settle for less?"
Because type errors in unit tested code are basically non existent. It's a fictional problem and as such there is no point in spending real resources chasing fictional problems.
There is plenty of research that shows that static typing gives no benefits (statistically insignificant) when it comes to software correctness.
What you are asking is why shouldn't the local government spend money on a Yeti patrol to protect the general public against Yeti's? The Yeti patrol will eliminate an entire class of problem (Yeti attacks).
>Because type errors in unit tested code are basically non existent.
this is short-sighted. I am not talking about trivial string-instead-of-an-integer errors here. powerful type systems let you encode far more sophisticated constraints on the program. safe rust makes data races into a compile-time error via its type system, for example. unit tests can't do that.
I think you're greatly underestimating what types can do for you. you seem to have this mental model where you would write the exact same kind code with a type checker as without one, and the only difference is whether you have to convince some pedantic bureuacrat that your code is correct when you already know it is.
but when you have a powerful type system, you don't write the same kind of code. the type systems helps drive design, similar to how tests can drive design. you have probably seen code that is bad because it wasn't written with testing in mind. there wouldn't be much value in adding unit tests to the code right away -- you probably need to do some highly invasive re-architecting to make it testable.
so, is it really such a stretch of the imagination that code can also be deficient because it's untypeable? perhaps the reason you don't see much benefit to types is because you didn't write the code with types in mind, as a design tool.
there is a learning curve to writing testable code. the same is true for types.
Sadly, it still took 4 days to clear the deadlocks out of my Rust program.
No, my mental model is removing the type checker allows me to write shorter, more concise and higher quality code. Dynamic typing enables much better code styles than static typing.
> the type systems helps drive design
Ahh you are an complexity merchant, if only I made my code more complicated all my problems would be solved. I'm afraid not, the more complicated your code the worse it is.
No I'm afraid, I have actual real commercial experience of doing both styles of software development, the dynamically typed code is the better approach by far.
It's not that I don't understand you, it's just what you are saying is a load of rubbish. :p
No. It is useful in almost every environment other than things like REPL. I can write TypeScript for hours without writing any tests, run it and fix a few minor logic bugs and code is production level, which is great for prototyping. No chance with plain JavaScript without typing -- it would be at least a day or two and I would constantly run into stupid typos or other errors that can only be found at runtime.
Good tests only render SST superfluous insofar as the person writing the tests is capable of never making a mistake. The thing about good static type systems is they don't typically make mistakes in the ___domain they work in. The fact that you don't have to muddy your test with a bunch of type checking logic and instead focus on the thing actually being tested is just the cherry on top.
High test coverage is useful in the environment where the code is not adequately typed. That's because static types adequate to make the code bulletproof will also detect the problems unit tests could detect, rendering high unit test coverage superfluous.
So, how often is poorly typed code out there? From the popularity of test coverage tools, it must be ubiquitous.
The turned-around argument has a problem: testing should reveal type bugs, but static typing (of the kind typically discussed for mainstream languages) can't reveal non-type bugs.
In a situation where extreme levels of testing occurs, the extra assurance from strong typing is minimal. In a situation where strong typing is enforced, extra testing is still very useful.
I don't think it's possible to test a sufficiently complex system that isn't somewhat restricted in the states it goes through, which requires typing.
Consider a finite state machine with N states. There are a priori NxN possible transitions. In many cases however most of them are impossible, and you really have only O(N) feasible transitions.
Sure if you make O(N^2) tests you don't need typing... but typing could have made many of those transitions literally impossible. You don't need to test the impossible. For a sufficiently large codebase, you want to limit as much as possible the amount of tests you need.
Also I have more than once identified issues in our testing framework because when I made stronger types I uncovered bugs that escaped our tests. Non-deterministic concurrency bugs are extremely hard to test for. So yes, testing can uncover type bugs, but typing can uncover untestable bugs.
This is a very arrogant and dismissive attitude to take. Consider that both pg and DHH prefer dynamic typing. Maybe you can think less of them as devs, but what each built is undeniable.
What have you built or added to the field that justifies such a lack of tolerance of those who don’t subscribe to your point of view?
> I am outright saying I won't engage in a conversation.
It's actually arrogant and dismissive because you say that in a way clearly intended to spark a conversation.
Also, pretty much all arguments for explicit typing suffer from mechanistic bias (see, https://www.youtube.com/watch?v=NmJsCaQTXiE&t=143s). I'm comfortable saying I just enjoy tinkering with a formal description of a data structure, because I'm a type enjoyer, not a type fan.
> It's actually arrogant and dismissive because you say that in a way clearly intended to spark a conversation.
I don't follow at all. Why would it be arrogant or dismissive to say something in an attempt to discuss it? I commented in good faith to an article that has a strong opinion ("willing to die on this hill") with a reflection of my own strong opinion ("unwilling to discuss").
At most you could say it was inflammatory, which wasn't intentional although looking at the insane number of child comments it apparently ways.
Similarly, choosing between 'int' and 'long' isn't part of a type system, unless the only semantics your data has is being an integer of a given width. Types are semantic, and telling me "height is 32 bits" tells me jack shit about units of measure or anything else.
Yeah I've been in that camp since like 2016. TypeScript is basically an essential for me now, and I don't debate it with anyone. If they don't get it, it just gives me a baseline understanding of how little experience they have in real world software development.
Sometimes the question isn't "are types good", it's "are the costs associated with static types worth it for this particular use case". And the answer is almost always yes; but the "almost" there is important.
Same here. Whenever someone starts babbling about why __language x__ is hard because of types or whatever, I just disengage. I don't care what anyone thinks about types. I use them, and I will keep using them no matter what.
if I store sql with a column definition that stipulates that the value must be an int between 1 and 3...what difference does it make if I accidentally create a corresponding variable with the value "fred"? it will never be persisted
furthermore, you can run in to even more confusion with a language type that is not properly aligned with the database type...which one wins? the db obviously, since language values not aligned with database definitions will never be persisted
So... about the strong part in 'strong static typing':
Many years ago I thought that it's a good idea for a game math library to have separate strong types for 'point' (a ___location in 3D space) and 'vector' (a direction and magnitude in 3D space), and allow/disallow certain operations (e.g. 'point + vector => point' 'vector + vector => vector', 'point - point => vector', while 'point + point' is illegal).
Sounds absolutely great in theory, but in practice it was a royal PITA to work with, but it took me much too long to realize this (how can it be such a hassle when in theory it's such a good idea!)
I soon went back to a general 4D vector class (where a 'point' is defined by .w = 1.0, and a 'vector' by .w = 0.0), and some debug-mode runtime validation (which catches things like trying to add a point to a point).
Of course strong typing also often makes perfect sense, for instance in a 3D rendering API it should be a compilation error to provide a texture-handle where a buffer-handle is expected, but after this experience with points vs vectors (which should've been a classic showcase for strong typing) I would never again "die on that hill" :)
TL;DR: static typing: yes! strong typing: it depends.
> For things like kilometers versus hours I'm still undecided.
Those help you more if you do a lot of math involving both them, but that’s also when it becomes a nightmare in most programming languages because of an explosion in the number of types.
Let’s say your code computes
3km × 4hours
If so, you need a “km hour” type.
In many languages, you also have to write code to make that happen, and to make it have the same type as
4hours × 3km
Even if you don’t ever store values with those types in variables, you also may need types for per km, per hour, km² and hour² for expressing the types of intermediate values (for example, in a physics computation, you may encounter √(3km²/4hour² to compute a velocity in km/hour)
And that’s ignoring that you may
- encounter minutes, meters, yards, etc.
- want to use algorithms that compute exp(3km) or log(4hours). What types do these have? Here, you probably want to forget about string typing values.
Apologies, I edited my post while you wrote your reply and removed the kilometers vs hours thing. But your reply makes perfect sense, and I think this "type explosion" is my main gripe with too extremist strong typing.
That's not a shortcoming of strong or static typing, that's just you realizing that your initial data model was flawed and that you either needed to unify them into a single type (like you did) or utilize an abstract class/interface.
The good thing about stricter typing systems is that it forces you to handle all the cases when you're doing a refactor like this before it compiles.
When changes to the data model happen in large codebases of dynamic code, it frequently gets shipped in a non complete state and turns into a production runtime error later down the line.
I don't know, that is so last millennium. If you prove your software correct, you won't need a static type system, it just gets in your way. And that's a hill I am willing to die on.
> I reject the idea that pg/DHH should be treated with any authority here, and I reject the idea that I need to prove my authority to you.
> That said, yes, of course my post is arrogant and dismissive. I am outright saying I won't engage in a conversation. I'm comfortable with that.
This is an extremely toxic attitude. I wouldn't be interested in working with you in any professional capacity, on an open-source project, or having you as a friend, and as a matter of fact if someone expressed this attitude at work I would complain to HR.
And then, kind of orthogonal to that, there’s also static typing and dynamic typing.
I think strong, dynamic typing is just as good as strong static typing (weak typing is objectively bad)
As long as the types of your variables don’t change from under you, that’s good, but I’m also okay with the compiler / runtime figuring out what those types are for me.
With "dynamic typing" there is no compiler to figure out the types for you. There is only the runtime to print a stack trace (if you're lucky) and bail out. The whole point of types is to be able to check things without running the program, because exercising every possible code path becomes increasingly untenable at scale.
A side (but many would also say critical) benefit of types is that they act as a form of documentation that can never go stale (because they are enforced by the compiler). "Dynamic typing" does not offer this benefit whatsoever.
> With "dynamic typing" there is no compiler to figure out the types for you. There is only the runtime to print a stack trace (if you're lucky) and bail out.
This is just incorrect. Plenty, if not most, dynamically-typed languages are compiled. Indeed even the idea that there's a clean dichotomy between compiled and interpreted is an outdated idea: I'm not aware of any production-quality language implementations which run tree-walk interpreters entirely without compilation. Modern "interpreters" typically compile to bytecode but in some cases even can compile to native code, they simply do so in a just-in-time manner. These compilation steps are quite capable of applying type systems.
For example, if you run a Python script, you can see the results of compilation cached in .pyc files (these will be either in the same directory as the source files, or in your __pycache__ folder, depending on your configuration).
> The whole point of types is to be able to check things without running the program, because exercising every possible code path becomes increasingly untenable at scale.
Is it? I would argue that the point of types is to report errors at the place where they occur, rather than doing the wrong thing silently and reporting an error elsewhere, or simply behaving incorrectly.
If a tree falls in a forest and nobody hears it fall, does it really fall at all? If a bug happens on a code path, and nobody exercises that code path, is there really a bug?
Many dynamically typed languages have static analyzers, but retain the flexibility of not needing to prescribe types up front (it’s impossible to know exactly what data structure you need at the start of a project)
That’s the crux of why I’m not all in on static typing all the time. (Especially for networked programs that expect a wide range of different kinds of input)
Having to prescribe the types I need before I actually need them goes against how I tend to build.
> Many dynamically typed languages have static analyzers, but retain the flexibility of not needing to prescribe types up front
Almost all static type systems have escape hatches that let you go dynamic when you really really need it. Also, I bet that most of your use cases for dynamic typing could be solved with a simple tagged union. Most people bemoaning the lack of flexibility of static typing just don’t know about how tagged unions can easily emulate dynamic typing when we need it, such that we rarely even need to reach for the actual escape hatches.
> (it’s impossible to know exactly what data structure you need at the start of a project)
Thankfully data structures are even easier to change with static typing: change it, gets a ton of type errors, fix them, done. With dynamic typing you run the risk of missing a call site.
> Having to prescribe the types I need before I actually need them goes against how I tend to build.
There’s type inference for that. I personally take advantage of it any chance I get.
Sounds like you'd like C#. It does type inference with `var` and similar things, so you don't always have to specify types explicitly, but everything does have a type at compile time.
> There is only the runtime to print a stack trace
Types are not exclusively for verification, and even if you decide it is, you can do a lot more with a type error than exiting the program.
That stance most people keep repeating is actually ridiculous. The most common usage of static type systems is to verify badly-written ad-hock dynamic ones that handle user errors.
Dynamic typing doesn't exclude the compiler from being able to detect some things that would cause errors at run time. It just means that these turn into compile time warnings, not compile errors.
In Common Lisp, it's common practice to not accept code unless all such warnings are gone.
That is static typing. You can add static types to a dynamic language, but then you introduce a compilation step. This is not an argument in favour of dynamic typing. If your language requires a compilation step anyway, then what you have is a static language with only one type: the ‘any’ type.
Every argument I’ve seen in favour of dynamic typing is some variation of “I like it this way.” They’re not technical arguments because dynamic typing is a strict subset of a statically typed language, equivalent to passing a flag to turn the type checker off.
Sure, not every compiler offers such a flag, but that is an argument against one particular static language (or group of languages), not an argument against static types itself.
Dynamic vs static typing is a similar level of design decision as interpreted versus compiled. It's ridiculous to try to claim one is universally better than the other as they both have styles of programming and domains they are better suited for.
I would never want a statically typed, compiled awk for example.
I generally think large, production, multi-developer software should be written in a statically typed, compiled language.
Strong vs Weak typing is a closer to a debate about having a name spacing system or not. There is really no great reason to have weak typing or not use a name spacing system.
Mainstream dynamically typed languages are moving in statically typed direction. Python (Mypy, Pyright and others), Ruby (Sorbent, Rbs), JavaScript (Typescript, Flow). How many statically typed languages optionally removed types?
It's a bit sad to me that people can only imagine writing code for large production systems. Static types are hugely beneficial in this area which is why we see this trend.
Personally I think Python moving in the direction of static typing is a mistake, dynamic typing is very useful for domains where python is strongest: modeling, statistics, scientific computing etc. It's also part of the basic design of Python to be a dynamic language. Likewise Ruby, with it's heavy use of metaprogramming, also benefits tremendously from a lack of types.
But let me be clear: I do think statically typed languages are a very good idea for large production systems, I just personally do a lot of programming that's not for these systems.
> How many statically typed languages optionally removed types?
I wouldn't say "removed" types, but I've been in software along enough to remember when dynamic typing was the big hot thing and crusty old Java devs complained that we couldn't possibly live without static type annotations. I distinctly remember when C# introduced `var` (which is of course really type inference, not dynamic typing) to appeal to devs that were growing weary of types.
There's a great example in SICP of implementing a full object system in just a few lines of code that would not be possible to implement as elegantly in a statically typed language. Do I want that for a production system? No. But there is, or at least used to be, a world of computation being done for reasons other that quickly getting PRs pushed out to prod.
Ok, maybe my previous message was too radical.
Of course there's a space for dynamic types (I use Clojure in free time), but there's no reason for static types to obscure your program. When I talk about static typing I mean Ocaml, Haskell, Scala, F# where you program is statically typed but you'll rarely see any types.
You should legitimately try a project in notepad. Not Notepad++, specifically Notepad. Probably a very short project, but you should give it a shot. It'll give you a bit of a perspective. And some real appreciation for Notepad++.
discussions are a two-way street. if you aren't getting it, then how do you expect someone else to "get" your position?
"The whole problem with the world is that fools and fanatics are always so certain of themselves, and wiser people so full of doubts"
-- Bertrand Russell
[edit: To clarify, i am on the "i like strong static typing most of the time but also understand their pros/cons wrt. dynamic languages" side of things]
Types are integral part of standard programming languages. If you mean "types bad" as in BASIC/Javascript variables, then yes I fully agree. These languages were never meant to be a solution for professional software engineering.
Once a language has proper types, it can be "weak typed" by not forcing "strong typing" by being dynamically typed in nature. If the language has normal preprocessor support, the static type system can be added on the project level. And then in essence you get a strong typed environment in a very thin programming language such as C.
Let's take a big C or C++ software project, the process behind it. Of course, the project is in those languages because it requires native opaque pointers and hardware access. The project has coding style, it has arbitrary rules. Although there is little to stop anyone from making a mess in C or C++ code there is entire code infrastructure and CI/CD chain around it. Lets say the rule is no void pointers without encompassing struct that's a type that can be checked via macro system. If you wrongly use the type struct, you get stopped by the project build. If you go around the rule, you get stopped by a code analysis after commit (and get in trouble for doing that).
It is because the enterprise TS supporters are too loud.
There are many here saying they consider people that dislike TS to be idiots and they don't lower themselves to discuss with those who think JS is preferable.
They don't wanna argue, we don't wanna argue. Life is great.
And decades of startups created with the next lisp/ruby/python/perl/JS. Sign me up for those. I for one would rather eat a bullet than maintain any of the TS I'm watching people write today.
I don't think we're near the peak yet. We're still on the way up. I've seen $5M projects turn into $50M projects in the space of 5 years and nobody else in my little consulting bubble has noticed at all lol. It shouldn't take 100 devs to build a SaaS with 5 forms and a dashboard but here we are...
Static typing sounds great to Junior Software Developers, who don't properly understand what the problems the software they write needs to solve.
Static typing allows me to check the types are correct! In practice, typing related errors are a very rare bug so it isn't very useful.
Static typing allows me to check things at compile type. In practice, this means waiting around for things to compile and lots of dead time. The larger the project get the longer the compiles times.
If performance doesn't matter, dynamic typing is pretty much always the way to go. You get the code out faster, so you have time to write more tests and you can throw away the code faster. Cause in the real world, code is a churning thing that evoles with the business requirements. Not some static build once thing that static typing and Junior Software Developers assume.
> or even worse not enforced even at runtime (JavaScript, I'm looking at you... 1 + "2" == 12).
I'm having trouble taking seriously a post that beats this dead horse.
Sure, it's there. It's even documented in the language specification, but now that we have template literals it's rarely used because truly, what is the expectation when using addition on two variables of different types?
> grug very like type systems make programming easier. for grug, type systems most value when grug hit dot on keyboard and list of things grug can do pop up magic. this 90% of value of type system or more to grug
> big brain type system shaman often say type correctness main point type system, but grug note some big brain type system shaman not often ship code. grug suppose code never shipped is correct, in some sense, but not really what grug mean when say correct
In my opinion static typing is useful and I wouldn't start a commercial project without it. That being said I think it's productive to practice programming in dynamically typed languages, because:
-You avoid the sort of type golf that happens with a sufficiently powerful type system(I regret every use of the `infer` keyword in TypeScript).
-The situation forces you to be radically explicit.
-You can explore situations that would normally be hard/impossible to type.
I have come to believe that static typing zealots haven't worked seriously on a large variety of (shipped) software projects. I certainly was one when I was in school.
Static typing is a good choice when the system you are implementing is already defined. It's great for implementing a well-defined algorithm or protocol, or a well-studied ___domain like game engines or financial exchanges or rocket ships. Basically, wherever correctness is necessary and possible.
Static typing is a bad choice when the system you are implementing is largely undefined, or actively evolving. That is, "the rest" of the software, where "software correctness" is undefined. Which includes things like a business, a new video game, a website, and scientific research. Because you will waste time building an ontology of types that can't possibly be known at the time, or worse, constrain the natural evolution of the project.
CS students often get a warped perspective of software, where everything is well-defined by their professor or some textbook or RFC or some other smart bloke, and they just have to implement it. In reality, the average software project is not this way.
The end result is Enterprise Java (formerly C macro hell). You get a bunch of CS grads who think static typing is the only answer working on some fuzzy business logic with Java. The conclusion is an unspeakable monstrosity of bad abstractions (and job security).
The research out there found no meaningful difference between both styles (unless there's newer research I haven't seen?) and people keep taunting around how their preferred side is undoubtedly the right one.
That's just like, your opinion man.
Personally I like typed languages but the type systems in languages like TypeScript are simply insufficient. You still end up with runtime bugs because you can't actually use those types at runtime. You can't encode a lot of the runtime logic into the type system so you're still manually checking impossibilities everywhere. I find myself even having to create local variables just to make typescript detect an obvious condition.
If a type system could basically remove the need for me to think about runtime bugs, then that's an absolute killer feature that I doubt anyone would argue against. But most languages don't provide that, so you're stuck in this halfway point where you have all this overhead, some benefits, but you still can't 100% trust your types.
As for why there are no meaningful differences in bugs, speed, etc my guess is that it all evens out. Without the type system safety net you are much more likely to test your code and as a result less bugs go in. On the other side people rely too much on the type system that's not good enough and then still end up with the same amount of runtime bugs. On one side you write code faster, but you have to test more, so it also evens out with writing more boilerplate, but with less tests.
I really wanted some hard research on this, but I know it's a hard one.