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 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.