Strong static typing, a hill I'm willing to die on
svix.com
svix.com
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.
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.
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.
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?
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.
Like most of us here it seems. But do you prefer TypeScript over JavaScript? What is your belief / feeling?
I'm currently dealing with the awkward tooling Python has for types :(
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?
Can you point to that research?
If there's more, I hope people reply here with it!
The evidence behind strong claims about static vs. dynamic languages - https://news.ycombinator.com/item?id=16287083 - Feb 2018 (1 comment)
The empirical evidence that types affect productivity and correctness - https://news.ycombinator.com/item?id=8594769 - Nov 2014 (25 comments)
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.
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.
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.
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.
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)
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.
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.
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.
TypeScript is not this; it's a type checking layer over a dynamic 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?
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.
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.
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.
Then, if you have converted lots of Javascript into Typescript, you have probably found several type-related bugs.
On one hand, this makes you (me) trust Typescript more, and feel safer knowing that your types are correct.
Later, you discover that a few of the declared types are wrong! You also still get "this" wrong in callbacks.
You could say that Typescript lets you try to get your types right, but you have to do the heavy lifting yourself.
In your example it would be:
type Age = Opaque<number, "Age">
And then you wouldn't be able to pass a random number to printAgeIt'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…
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.
Even when dealing with third-party code, or joining an existing project?
> 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.
Also, if I am using an API another team is providing - I don't want to have to go and trace through their code.
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.
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.
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.
In any large statically typed program, 2 out of the 3 lines exist to make the compiler happy rather than the software developer.
Static typing is a terrible terrible way to develop code once you start measuring it objectively.
I use Scala and the syntax is pretty slim to get very rich typing. Love it compared to non-static typing.
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.
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.
https://stackoverflow.com/questions/52376716/c-overloading-o...
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.
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.
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.
JavaScript is terrible because it was built over 3 decades by different competing browser vendors.
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.
Oh, look. Something that statically typed languages solves.
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.
Dynamically typed languages are more powerful languages that can handle problems like this correctly.
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.
> 9223372036854775807 + 10 :: Integer
9223372036854775817
>>> numpy.int_(9223372036854775807) + 10
<stdin>:1: RuntimeWarning: overflow encountered in long_scalars
-9223372036854775799You'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?
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`.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.
>Both programs have the same underlying structure No they don't, this is just a massive gap in your knowledge.
> try refactoring Python code without type hints in a large code base
I've done that on multiple occasions, the test suite is fine, thank you very much.
That's simply not true.
> No they don't, this is just a massive gap in your knowledge.
Based on your responses here I don't think you really know what you are talking about.
> I've done that on multiple occasions, the test suite is fine, thank you very much.
Well, if you say so.
(30+ years of experience here working on million+ lines systems).
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.
And note the switch from 'only' to 'any'.
Anyway just hypothesising.
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.
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.
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.
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.
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.
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.
> 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?
[0] https://developers.slashdot.org/story/18/01/01/0242218/which...
A Large-Scale Study of Programming Languages and Code Quality in GitHub (2014) - https://news.ycombinator.com/item?id=15378800 - Oct 2017 (66 comments)
It's also discussed prominently in Dan Luu's survey, linked to here:
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 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.
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.
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!).
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)
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.
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!
--
[1] https://www.cs.yale.edu/publications/techreports/tr1049.pdf
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 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.
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.
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.
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.
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.
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.
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.
Yep. In my experience, hardliners tend to not be great engineers, even if they have decades of experience.
hardliners tend to not be great engineers
100% agree."Strong opinions, weakly held" is okay with me though. I don't call that "hardlining."
Although, sometimes it's hard to differentiate "strong opinions, weakly held" from "strong opinions, strongly held, not open to new ideas."
> 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 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.
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.
You know everything by heart?
> 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.
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.
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.
---
0: https://news.ycombinator.com/item?id=37768953
1: https://www.twitch.tv/rw_grim
2: https://www.twitch.tv/j_blow
3: https://www.youtube.com/watch?v=rysgxl35EGc
4: https://github.com/django/django/blob/main/django/db/models/...
5: https://docs.djangoproject.com/en/4.2/topics/db/aggregation/
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.
In JavaScript, there are both, and a third one besides, largely for reasons of historical accumulation and inconsistent implementation:
String.prototype.substr(start, length?)
String.prototype.substring(start, end?)
String.prototype.slice(start, end?)
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.
(Related reading: https://stackoverflow.com/questions/2243824/what-is-the-diff....)
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.
Static typing makes it easier for my computer to get me the relevant docs.
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.)
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 say that as someone that quite likes static types. I don’t care, do what you do!
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.
For general large projects... I feel your pain.
> 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.
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.
We have large python codebase, all sorts of linters and checks.
Still have developers do something like:
# we have a list of things
# but we only need one
try:
a_thing = things[0]
except IndexError:
pass
assert isinstance(a_thing, Thing)
Sure maybe it's a junior thing. But it still happens, and the typing doesn't save us. I don't know how to tell juniors to stop doing thisWhy 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.
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.
That said, I agree 100% about the importance of fast feedback loops.
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!
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
“Parse, don’t validate » is a common Googleable idiom
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).
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.
Perhaps you are thinking of this great blog post on the subject https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
I saw similar, catchy phrase: “parse, don’t validate”.
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
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.
Is it really? Map<String,String> is a totally reasonable type to use in certain cases.
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.
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.
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'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".
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.
That's just because the code is bad, not because the type is bad.
That's literally just the definition of, this code is so bad, let's just not type it.
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.
I really wonder how people can actually work like that. Is there some overarching methodology like super strict tests with 100% code coverage or so?
foo[`asdf{some expression returning string}`](args)
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.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.
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++ projectYes, 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.asdf123You 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.
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.
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
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.
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.
> 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.
Types lead to less bugs - nope, maybe marginally, but not significantly (see research on that)
Types lead to a better development experience - nope, I have all the definitions and vars in my REPL and so has my IDE. I get completion on all symbols, I can look at call trees, usages, refactor things with confidence, run functions in isolation, replace them, wrap them, all from the REPL and inside my application.
We encode everything in the type system - you can't. You need to do runtime validation.
And good luck untangling your type definitions when your requirements change. That one is the killer. Static types are premature concretions on the domain data model as you understand them now. They will change and if you are unlucky you will have to support multiple different variations of domain models in the same runtime. Good luck with that. Especially if you use inheritance.
Granted, static types give compilers great leverage to optimize code, but, wouldn't you believe: there are dynamically typed languages that have static types as a la card add-on.
But for many, many use-cases, especially enterprise development, a dynamically typed, immutability-first, functional language pays off dividends in the long run.
The categorical error that is made by many strong typing fans is that they assume you would write the same code - just without types. Well, you don't.
This is great, but it only works if you are executing the code that you want to inspect/complete/refactor. In my experience, that just doesn't scale well.
> You need to do runtime validation.
Type systems don't eliminate this, sure, but when used correctly, they _drastically_ reduce it, which is extremely useful.
> And good luck untangling your type definitions when your requirements change.
That's the best part - the compiler tells me exactly what I have to fix to get things working again. If I do the same thing in a dynamic language, I have to track things down myself, wait for unit tests to fail (hopefully I have 100% coverage...), and pray that there isn't an escape.
Just to take one somewhat at random:
> And good luck untangling your type definitions when your requirements change.
I think it is exactly the opposite of the case that static types make it harder to adapt to changing requirements. In every dynamically typed system I've ever worked on, there have been important assumptions about data structures - sometimes checked dynamically in pre- or post- conditions, sometimes only checked in tests, and often simply not checked at all - scattered all around, such that changing requirements necessitated reasoning through the impact of the change on all these implicit assumptions. It was terrifying to make changes. Sure, easy to make a change and start up the application with the new code without a fuss. But to know whether the change broke some obscure codepath I hadn't considered or been aware of? Very difficult! I much prefer a static analysis pass saying, "hey, you changed this interface, did you know this codepath over here relied on that thing you changed?". Static type systems are not the only way to accomplish this, but in my view, they are a much less burdensome way to accomplish it than the level of dynamic validation and testing necessary to do so otherwise.
But again, it's just a frustrating debate, because clearly you have the exact opposite experience! So where does it get us? Nowhere really...
Well, that and PTSD from digging through so many horrors created with Python and JavaScript.
I actually found this way easier with static types because with static types I could be significantly more confident about every single place one of those types was used, and look at it and see if it needed changes. This was way fiddlier to do in a dynamically typed environment
JavaScript is inconsistent and weird, but Netscape's legacy long since stuck us all into it.
I can see in a very OOP-heavy style maybe, with huge nested classes, that compile/parse time static typing saves a lot of mistakes - but I've come to see OOP as mostly a disaster, and simple functions and structured data almost always win the simplicity and maintainability battle. And modern language servers / IDEs can catch a lot of typing errors during JS / Python development.
I'm persnickety about a lot of programming stuff, but never had a strong opinion about static vs dynamic typing. Both have millions of successful projects under their belt.
There's a simple fix for that, just do keyword argument only everywhere you can.
For me the hill I'm willing to die on is that labeled/named/keyword arguments are absolutely necessary and most of the usefulness of types is in fact just a shitty version of labeled arguments (god help you if you have a function where two or more arguments have the same type!)
As for args with the same type, you can use phantom types I guess but I haven't explored it much in python. I'm pretty interested in the dfdx library in rust which can type enforce matrix or vector operations on it's shape. Stuff like that would really help me
Java was a huge boost, by eliminating the cognitive load of manual memory management.
Scala can be used as a "better Java" with less verbosity, but also adds a number of extremely useful features; Java has slowly added some of them into the language over the last 8 years.
JVM tooling IMO is fantastic, it is one of the largest open source communities, and there are many options if you don't like Gradle or Maven. Also, as I understand it, the dependency management systems found in languages like Python and Node are horrible in comparison to the JVM, where it is usually not a problem.
e.g. in Python/Ruby I had a REPL and used it a lot. Go doesn't really have a good REPL, so I tend to not use it so much. I probably pay some in productivity for that?
On the other hand, I type code like:
name:=fun("foo","bar");if name=="foo"{fmt.Println(name)}
And write and let gofmt sort it out. You can kind of do that in Ruby in theory (not sure if good formatters exist), but Python is more tricky (and I also think the auto-formatters for Python are bad and often make code worse).The flexibility of things like Active{Record,Model} in Rails are great, and hard to emulate with Go. People have tried, and it just doesn't fit with the language very well.
Go's simple "just compile to a statically linked binary" distribution model probably saves me time, especially for distributing it to other people.
Probably other differences I can't think of right now.
So between all of that, how do you split out the productivity of "types" vs. anything else?
That's also my problem with the research on this, because you just can't, not really. What you need to do is compare e.g. Python with Mypy vs. without. I don't think anyone has done that(?) And even then you run in to the problems with differences in tasks, differences in programmer experience and ability, etc. which matter because for a lot of these studies n tends to be small.
I'm firmly in the latter camp. Because the compiler can't check program correctness only type correctness. Type correctnes is necessary for program correctness, but NOT sufficient. Static typing adherents fail to recognize this, thus falsely believing that static typing guarantees them more than what it actually does (blub).
Consider the article's birthdayGreeting example. Author is happy that static typing catcges the birthdayGreeting("John", "20") bug because "20" is not a number. But birthdayGreeting(" ", 123) is not caught (" " is not a name) and neither is birthdayGreeting("Anna," -12335). birthdayGreeting("Anna" 4.5), though, is caught, which arguably is wrong since 4.5 is an age.
This is actually very important since "type bugs" are trivially easy to catch, while "semantic bugs" can stay undetected for years. An account balance stored as a uint that overflows, a number that must be prime at a certain program location, but isn't, a list that must not be empty, etc. No, not even dependent types can guarantee these invariants.
If you don't believe me, read up on some catastrophic bugs. Those that have caused space ships to explode and cars to crash. To the best of my knowledge, not a single one has been caused by bona fide type errors. In the overwhelming majority of cases, the root cause has been a semantic error.
[1] - Most people don't understand that typing is measured over at least two axes, strong/weak and static/dynamic, and continue to conflate weak typing with dynamic typing. C is statically typed and weakly typed. Python is strongly typed and dynamically typed. Javascript is weakly typed and dynamically typed.
Author here. I think your examples illustrate my point rather than provide a counter example.
I mentioned later in the post that we validate when creating the type (from e.g. user input) so we know that the `Name` type is always valid (and " ") is not a name. So the type ensures that we have a valid name. So this will definitely be caught in our codebase.
As for birthdayGreeting("Anna" 4.5) and birthdayGreeting("Anna," -12335), number means a float in JS so it actually would be valid, though admittedly I had full integers in mind when writing this. Another example of where stricter typing than what TS providers (which Rust does!) would have helped better define the invariant.
So in short: the problem in the code there was that I was trying to show a simple example, which means I didn't define all the types as strictly as I normally would have, and it surfaced more bugs that would have been caught with types.
> This is actually very important since "type bugs" are trivially easy to catch
This, IMO, is exactly why I’m in the static type camp. It’s so trivial that you can do it declaratively, inline with the code it’s testing, with instant feedback, at every call site and in every downstream expression/statement. A type annotation doesn’t mean my semantic/domain logic is sound, I still have to test that. but it could replace dozens of trivial tests orthogonal to the logic I care about… tests which, let’s face it, few people are going to write exhaustively otherwise.
But to the point of triviality, static types tend to lull developers into a false sense of security. There are limits to their usefulness. You still need both runtime checks and test-time assertions for your logic. You just need less of them and they provide more value per line of code. But I've never seen a type system capable of expressing the entire problem domain in an ergonomic way. Not even close.
For my money, a combination of: static types, property-based testing, unit/integration tests, and an end-to-end test against prod are all required. You can compensate for the lack of one by putting more effort into another but you're really just shuffling the "correctness burden" from one place to another. I like static types because it makes the rest of testing easier. But at some point, you've got to roll up your sleeves and make sure it works IRL.
On large projects this can be a problem. I worked on a Python server some years ago, and with every large program that supports exceptions, there's a catch block somewhere that silently eats the exceptions it doesn't process. Well, some modules were loaded dynamically, so my first spelling mistake (and second and third...) took quite a while to debug because things just didn't work, the code didn't get executed, but there were no errors. This also ate syntax errors, too, since that is also an exception. So if you edited the file, you had no idea if things actually still worked (or even compiled!) until you tested the functionality from the file.
How is this evidence for it being a waste of time? No solution is perfect yet many are worthwhile.
One of the most famous type errors in recent history did in fact crash a spaceship.
https://en.m.wikipedia.org/wiki/Mars_Climate_Orbiter#Cause_o...
It's funny you should mention this, as it immediately brings [1] to my mind. This well-known incident was due to type-checking, with acceptable ranges defined ahead of time for some types (which resonates with your comment: make birthdayGreeting accept ranges 1-150 or something, easy to do in ADA).
There are a few more well-publicized issues with spacecraft that could have been caught with better type-checking, including metric/imperial conversions (why not build the unit into a type? Though the fault probably was with integration testing for [2]).
Of course, you are right that type checking can't find every code issue (especially algorithmic), it doesn't replace tests. Instant feedback and type hints are invaluable during the development phase, though.
In your example, one could probably replace the first name string by a person type/object, by the way.
No, even more types is not the solution. It's not pratical to wrap every measurement in a Meter or Inch type (standardization is one issue, is my Meter type the same as your Meter type?) and it doesn't significantly increase the amount of invariants checked. You still have range issues and you still won't get a statically check PrimeNumber type.
I'm not familiar with ADA, but VHDL has similar range constraints which are runtime checked.
It doesn't strike me as inherently unreasonable to use fixed point. At any rate, there are languages with built-in unit systems, so you could declare something like float length = 4.5 <m> and get an error if you try to add it to something in a different unit without having to roll your own dimensional analysis framework.
Obviously it doesn't, since you can make a statically typed language program where every variable is a string map and every function takes variable args, and have no help from your compiler at any time because every type is always the right type. Sounds stupid, but in fact this is what happened in the examples.
The examples are bona fide type errors, because even though a statically typed language is used, multiple different types of numbers got the same static type causing the issue. So the people here were offered the solution to their problem from their domain (two types of numbers), choose to use dynamic typing (make both a float even though they're different things), causing the compiler to have no way of statically checking their code, and that caused a bug. It's the opposite of a counter example.
It is in fact the absence of enough typing that caused these issues. How much more typing do you need :)
Ultimately bugs cannot be prevented by having a type system. We make mistakes regardless of static or dynamic types.
We can argue it's because of the lack of types, tests, oversight and so on. I'd argue nothing can force a programmer to design a perfect system.
>The examples are bona fide type errors
I'd disagree because those are logical errors. You can represent those as different types with a type system, but you could also not use a type system and convert those values properly. Whether we represent them as different types or not is an implementation choice.
Those can be considered "type errors" (or missing types? a type of primitive obsession?) in a typed language. But it's still an issue with the implementation's logic.
Ultimately deaths cannot be prevented by seatbelts. People will die regardless whether they wear a seatbelt or not.
We can argue its because of a lack of seatbelts, roll cages, airbags and so on. I'd argue nothing can force force a person to be perfectly safe while driving.
> The examples are bona fide safety issues
I'd disagree because those are driving errors. You can present those as cars missing safety features, but you could also not use those safety systems and just drive carefully. Whether we use safety systems or not is a design choice.
Those can be considered "safety issues" (or missing safety? a type of safety obsession?) in a safety design perspective. But it's still an issue with the driver's behavior.
bit of hyperbole for flavour:
If we could just ditch the heavy roll cage, we could go much faster!
But sure, let's pretend this is a real argument.
Typed languages are not at all like seatbelts. They aren't an easy safety feature you can use within a second and suddenly X% of bugs are prevented. No research shows that it prevents the bugs we actually care about, and they don't deal with runtime issues that actually kills people like the one discussed in this thread.
Type checking is a programming safety feature, and its trivial to argue in any domain that a safety feature is optional. Its always trivial to argue this because safety is never a functional requirement, so it is never guaranteed to be needed for anything, and its easy to point out that regardless of safety, mistakes will happen, and that safety does not prevent all mistakes. All of this is trivial, and can be applied to any safety measure, this is why I brought up seatbelts: to show that your argument depends strongly on the context of where the safety applies, and the sacrifices that have to be made to gain that safety.
Which brings me to my second point: safety is always a tradeoff. Saying things like "perfectly safe" is completely void of meaning since nothing is ever perfectly safe, basing an argument on the fact that we cannot be perfectly safe is saying we shouldn't care about seatbelts because we're all gonna die anyway. Safety is a tradeoff and you should consider the up and downsides when you talk about it, always. So on your point:
> They aren't an easy safety feature you can use within a second and suddenly X% of bugs are prevented
Actually thats exactly what they are, because they will stop you from compiling a program that would have bugs in a dynamically typed system. Do they prevent all bugs? No, nobody is saying that. Its also easy to find how, just by googling: https://stackoverflow.com/a/27791387
> No research shows that it prevents the bugs we actually care about
No research shows anything about software, there is no research on software, stop deflecting.
> they don't deal with runtime issues that actually kills people like the one discussed in this thread
This thread is full of people suggesting type-safe fixes that would've prevented this, I'm not sure how you're still arguing that typing wouldn't help here. Again, maybe you don't want to use it because its too much work or some other kind of tradeoff you don't want to make, fine. But if you keep suggesting that types wouldn't work here you simply don't understand types.
I've never said that. It's always about trade-offs, but simply adding a typed system would not have prevented the issue, as it was already built in a typed system.
My whole point is not pro or against typed languages. It's that we need to move beyond that.
> Actually thats exactly what they are, because they will stop you from compiling a program that would have bugs in a dynamically typed system.
It totally depends. It only stops you if you are diligent enough to write programs with a very good type structure and hopefully you don't have any logical mistakes. If you type everything with primitives like uint you can still have a compiling program that's incorrect.
> This thread is full of people suggesting type-safe fixes that would've prevented this
And I could suggest fixes without typed languages that would also have prevented this, like better testing. The point is that a type system does not inherently change anything. It's still up to the developer and the systems around to be good enough to prevent catastrophes from happening.
Using types is most definitely one way of solving the issue, if implemented correctly. But the exact same could be done with a dynamic language.
> But if you keep suggesting that types wouldn't work here you simply don't understand types
Sure... I don't understand types ;)
In any case, I never said types wouldn't work here. Read my comments again. I said types are not the determining factor. The fact that this happened in a typed language simply cannot be overstated. Using a typed language or not affects many variables but it's not what will ultimately decide if a critical piece of software will kill someone or not.
Ok, once you know where to move to we can consider that as a good solution. Until then, lets just use types, its the best tool we have.
Restated: car has seatbelts, seatbelts were not used, people got injured, hence "seatbelts were not a deciding factor, we need to move beyond that". OK, sure, I can say the same of whatever you intend to move to, and we will have wasted a lot of air to no benefit.
Types are not runtime representations, and any good nominal type system will allow you to avoid this sort of problem easily. For example, this Haskell code
newtype Inches = Inches Double deriving Num
newtype Centimeters = Centimeters Double deriving Num
a :: Centimeters
a = Centimeters 1.0
b :: Inches
b = Inches 1.0
fine = a + a
alsoFine = b + b
mistake = a + b
will give this type error Couldn't match expected type ‘Centimeters’ with actual type ‘Inches’
In the second argument of ‘(+)’, namely ‘b’
In the expression: a + b
In an equation for ‘mistake’: mistake = a + b
but both `a` and `b` will be doubles at runtime.An area, inch^2.
> What is the type of Inch / Inch?
A unitless ratio?
I don't understand why this is not practical in safety-critical software. nyssos demonstrated what I believe is the right way to do it.
Do they suck more than losing spacecraft?
some_duration = minutes(3) + seconds(2)
Of course that doesn't change the fact that any function receiving a duration would only raise type errors at runtime. For instance, someone could try to pass the number of seconds as an integer rather than as a duration: remind_me_after(182)
But any semantic testing would automatically catch type errors as well. remind_me_after would fail every single time assuming the implementation would be something like this: def remind_me_after(duration):
reminder_api.set_reminder(now().as_seconds() + duration.as_seconds())
I think this is the reason why studies don't show huge differences in defects between dynamically and statically typed languages. You have to do semantic tests anyway, and they would cover most* type errors as well.What would it mean for code to be semantically correct while containing type errors? It would probably mean that the code is not in fact semantically correct because some semantic edge cases are not covered by the tests.
* Not all of them because semantic tests could theoretically succeed by accident for some specific values that have incorrect types.
It can also mean that the code has incorrect subprograms, but that the program as a whole is protected from them by some hard to analyze condition. As an extreme example, one of these two programs "works":
if goldbachConjectureCounterexampleExists() then print("Hello world!") else launchMissiles()
if goldbachConjectureCounterexampleDoesNotExist() then print("Hello world!") else launchMissiles()
but I don't know which and neither does the compiler. Rejecting them is a feature, not a bug.They would not always be stored as flat floats. You could have type aliases for both values (type alias inch = float, the same for cm) and have not a flat float but an inch or cm as a type. Or, even better, F# has type support for such values: https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
Yet static typing does very much catch issues, so I really don't see where you're coming from with this.
what? in a language with ADTs and exhaustiveness checking, accessing a key from an associative array will give you back an Option type and the compiler will force you handle the code path where the key is missing.
unless I've misunderstood what you meant, this is a canonical showcase of what modern static type systems are good for.
and in your parent post:
>a list that must not be empty, etc. No, not even dependent types can guarantee these invariants.
what do you call haskell's NonEmpty then? https://hackage.haskell.org/package/semigroups-0.18/docs/src...
data NonEmpty a = a :| [a]
I think you're underestimating what static types can do. it's funny you mention "blub" in that post ...It's mostly the same with nominative NonEmpty types. Any value the programmer has instantiated with one of NonEmpty's type constructors have type NonEmpty - everything else don't. For sure, you could create a NonEmpty class in Java too, but it would still be up to the programmer to enforce the type's semantic during program execution. If the variable L has type NonEmpty (list) then it's bloody obvious to a programmer that the expression pop(push(push(L, x), y)) also has type NonEmpty. To a compiler figuring that one out is mostly impossible.
Option types serve other purposes, like composability; tracing program flow can determine the points where a nullable type either (a) will always be null, or (b) will never be null, and many type checkers for type systems with nullable types (or, more generally, non-discriminated unions, nullable types are just one example.)
I honestly don't know of a good way to implement this pattern without type checking or at least optional type hints and static analysis. Instead, you can do validation of the input values in every single method, which is hugely burdensome, or you can assume callers are passing you valid values and write tests to make sure it isn't too catastrophic if they don't. I don't find either of these solutions satisfying.
If you program in a dynamic language, then you have no choice but to perceive all bugs as runtime bugs. Because every bug you witnessed was a runtime bug, so why would types have helped you?
When I program in a typed language, the compiler rejects programs I write all the time. Things like: putting code that can't be rolled-back in the middle of a transaction.
Uhm. What? I'm a static typing proponent and I have never believed that the compiler will guarantee program correctness. Nobody believes that. They believe it largely eliminates a certain category of error, which it does. You're entitled to your opinion on dynamic vs static typing, but this particular point of yours is just a wild straw man
That's just not true. Absolutely no one claims that static typing guarantees correctness, because that's ridiculous and obviously nonsense.
As for function declarations and structure fields, that's where you need type information to read the code. Once a program gets beyond a few hundred lines or beyond one developer, some amount of annotation is essential.
The main objections come from Python and Javascript users, of course. Python retrofitted a very strange advisory typing system, and Javascript retrofitted TypeScript. Both are bolt-on type systems and are used in mixed typed/untyped environments. That is painful.
LISP also got a type system retrofit decades ago, with "flavors" and the Common LISP Object System, and that was ugly, too. The lesson here is that retrofitting a type system creates a mess.
The bigger issue with Python and static typing is the ecosystem and conventions that a lot of developers use, made worse by a lot of these developers really being data scientists who are writing Python. *args/**kwargs are heavily abused by people too lazy to write proper method signatures. It's extremely common in Python to have methods pass around DataFrames or dictionaries as a grab-bag of stuff. Bonus points when methods add and remove columns/fields so you don't know what's in the data bag until you run the code (or read every line).
You can do this in almost any language, of course. Nothing stops you from writing a C# program with every type being `dynamic` or have all your Go methods accept `interface{}`. But Python, for a long time, actively encouraged this approach, and sadly there's still many beginner tutorials today that present things like "just write a method that takes *kwargs and you don't have to change the function signature!" as an advanced language feature for smart people instead of an awful footgun.*
Despite that, TS is still quite cumbersome to use due to its initial mission statements. I.e. `Object.keys(myObject)` doesn't return `keyof (typeof myObject)`, so if you'd do something like this: `Object.keys(myObject).forEach(key => myObject[key])` it'll throw an error that key is not assignable to `myObject`. The reasons lie in design choices made in order to be able to gradually migrate an untyped JS codebase into a TS one. There's tons of issues like this, some of them are just completely absurd and make developers that try to achieve simple things throw their laptop out of the window :D
`<T>keys(o: T) => keyof T` is actually quite incorrect, because you can't guarentee that keys() doesn't return a value not in `keyof T`, a key could be from a subclass of T.
I think TypeScript is doing the right thing here in not claiming something that's very easily violated.
Flavors and CLOS are not "type systems". They are "object systems" and there is nothing "ugly" about them. Flavors was introduced into a Lisp which did not have a type system. Later CLOS was added to a Lisp which already had a type system: Common Lisp.
Fact is: the “types lead to fewer bugs” claim is just a feeling, it is “truthy”.
https://blog.metaobject.com/2014/06/the-safyness-of-static-t...
And it’s not for lack of trying. You might say the claim has been disproven.
That said,I personally like static types, but primarily for the documentation effect that is, maybe not entirely coincidentally, the only positive effect for which there actually is solid empirical evidence.
Why this is we do not know. But we do know that it is true.
Concretely-speaking, an example of a bug being preventd by a typing system: Removing a field from an object in a large vanilla JavaScript project is inherently a minefield that has caused many bugs in the past, whereas the same alteration in a full Typescript project can be made with confidence.
But not in the way you fervently want to believe.
Once again: people have tried to show the truth of this for a long time, and they have consistently failed.
> This paper presents an empirical study with 49 subjects that studies the impact of a static type system for the development of a parser over 27 hours working time.
There are some studies like that, but there are others as well.
And remember that all these studies were trying to find a positive effect, but failed to do so.
But I am wiling to have my mind changed. I would be really interested in seeing a study that failed to find a benefit of type systems for refactoring tasks in large projects. The example I posted in my initial comment seems pretty open-and-shut to me, so I'm curious.
That turns out not to be the case.
> I would be really interested in seeing a study that failed to find a benefit of type systems for refactoring tasks in large projects.
You're inverting the burden of proof here. People who claim that something is beneficial are the ones who have to show that it is.
Making a claim and then shouting "prove me wrong!" is not how science works. Not even computer science.
What I meant to convey is that the matter is settled enough from my point of view that there needs to be a very strong case being made for me to be willing to engage in that conversation.
Thanks for pointing this out, I've amended the comment to clarify a bit.
Based on what evidence?
Apart from emotion?
Removing a field in a data structure is such a big problem, even in typed systems, that the most ubiquitous serialization IDLs, and customer-facing API conventions, don't even allow it.
As far as serialization IDLs and API conventions go, they have the very special constraint of having to provide both forward and backwards compatibility across multiple applications, making them a very poor universal example.
As an example, I was once refactoring a Java system. The compiler was happy after about a day, the unit tests after three days.
So if you claim your type system is going to keep your refactoring safe, I'd say that I'm not the one being delusional.
And of course unit tests also tend to, in practice, catch the errors that are caught by types. If you check that a value is equal to 10 rather than 9, you've also implicitly checked that it is not of type PinkElephant.
Now I perfectly understand the feeling that people who are used to the safety-net that static typing appears to give have when encountering a code-base without types. I have the same feeling when encountering a code-base without reasonably comprehensive unit tests (that fail when I poke into the code-base).
The feeling is absolute terror.
I just contend, with reasonably good justification, that this feeling is not particularly justified in the case of static types, because the evidence is just not there.
I haven't checked what the evidentiary status is for unit tests. But then again, I don't make these sorts of categorical claims for unit tests. I just make claims for my experience with unit tests.
For every intent and purpose, the person I'm replying to is, with an equivalent level of straw-man-ness, which is why I phrased it this way.
> So if you claim your type system is going to keep your refactoring safe, I'd say that I'm not the one being delusional.
No, I'm claiming that there are entire categories of bugs that are protected this way, not that it provides universal safety.
> And of course unit tests also tend to, in practice, catch the errors that are caught by types. If you check that a value is equal to 10 rather than 9, you've also implicitly checked that it is not of type PinkElephant.
It provides that if and only if the code invoking the tested function is called with arguments using the same semantics as the ones exercised in the tests, which is impossible to enforce in a dynamic language. I'd even go so far as saying that it is axiomatically impossible for unit testing to be a truly reliable tool in such languages because of the lack of precondition enforcement.
Er, no. I certainly wasn't, and the person you were replying to above that was making the opposite claim: that it's harder than that.
And no, the type system doesn't really help all that much, once you have unit tests. Because unlike the unit tests, which checks values (and obviously the types as well, incidentally) the type system only checks the types.
So, trivially, if you mix up addition and subtraction, your unit tests will rightfully balk, whereas your type system is going to be "works for me".
As I wrote elsewhere in this thread, in my personal experience the type system was happy during a large refactor long before the unit tests were. Which means that there was a lot of stuff that the type system missed.
And so if you feel your type system is going to make your refactors safe, well, good luck to you and the poor people who have to use your code.
Unit tests can only check outputs for the inputs that are provided during testing. Leaving the input unbounded means that they can only provide guarantees for code that conforms to the tested subset of inputs.
I may be able to test that `foo(3) == 4` and foo("foo") == "bar", but since I can't guarantee that `foo({})`, or literally anything else, will never be called, there will always be holes in the safety net they provide. This is something type systems directly address.
> And so if you feel your type system is going to make your refactors safe, well, good luck to you and the poor people who have to use your code.
safer. It makes them safer, not automatically safe.
Its like a grep, except it can, for example, differentiate between "name" (of user) and "name" (of project)
I don't think you even understood the point at all here. "Types let me make guided and exhaustive changes to code that's much easier to understand due to the types and compiler support"
The objection is not that all bugs aren’t eliminated. The objection is that there is no empirical reduction in bugs.
You should have spent the time statically typing your code on writing more tests.
Rust has a more powerful type system that actually fixes MOST of the memory bugs in production
¯\_(ツ)_/¯
Smalltalk is memory safe and dynamically type-safe. That is, you cannot invoke an inappropriate operation. You can try to do that, but the system will refuse to do so.
I believe this would count as empirical evidence of very strong static typing reducing bugs. I think this points to what many other comments have been getting at, which is that typing is not a binary yes/no question, but a large, multi-axis (static/dynamic, strong/weak) spectrum with lots difference between type systems, as well as big differences in how people apply those type systems to their problem. You could still work in a static, strongly typed language and represent everything as strings, converting back and forth as necessary, but that's essentially working in a dynamically typed language. You could also take advantage of the tools the type system gives you in order to reap the benefits, creating classes representing the legal values and asserting important invariants.
Not to mention that range checking is also fixable by using optionals rather than assuming the existence of a value.
I’ve worked on reliability at a company everyone has heard of for software that billions use daily. So many bugs would have been blocked by having a type system at all (dynamic languages on backend or JS on web) or by having a less bad type system (changing ObjC to Swift and Java to Kt on mobile). Null pointers/references alone have created so many bugs for us. Bugs that could have been compiler errors.
It makes me so much more productive, by orders of magnitude. And I say this as someone who has worked extensively in several different languages which covers large parts of this spectrum, like C, C++, Java, Python, JavaScript, TCL and others.
It makes it much easier for me to reason about code I have not recently worked on, be it in the current project or in dependencies. It also means I can be way more focused on the problem at hand, since I'm not constantly having to go on tangent goose chases to figure out what exactly I can do with this object that some function returns.
Sure there's the good, fuzzy feeling when the compile doesn't error out. But that's secondary.
is exactly the reason why you shouldn't be use static typing. We have a developer here who doesn't document any of their code and thinks that a good thing. Whereas in the dynamic typing world, they would be forced to use the good practice of documenting their code.
Code is never self-documenting, there are research studies on it.
But the types provides for let's say code-level self-documentation.
I can look at a function definition and see it returns an IReadOnlyStream. Well if I'm familiar with IReadOnlyStream I already know what I can do with that. If not I can quickly find the declaration, and based on that I can get a good overview of what I can do. If not I have something explicit to search the documentation for.
With languages such as JavaScript I learn nothing by looking at the function definition, and almost always have to dig deeper. If it's an unfamiliar code-base, then often much, much deeper.
You would be better off using dynamic typing and returning an Iterator.
The dynamic typing route of a small number of language defined types is far better than any developer declaring their own types whenever they want.
Why? Because it's simpler and simpler code is faster to write and easier to test.
The point is that most of the time I wouldn't know it's returning an Iterator, because it's poorly or not document. At least that is my experience.
Anyway, I was only speaking for myself. And my personal experience is exactly the opposite: I'm far less productive in dynamic languages when working on any non-trivial codebase, for the reasons I stated.
Seems like a comparison of Smalltalk using an IDE and Java using Notepad?
Not really credible is it.
How would you feel if I claimed that you could make emotional arguments faster than you could make rational arguments?
Because that is how this debate feels!
I want the simple rationality of my compiler saying "no" if I try to treat my hashmap like an Apple or a String.
It’s very easy to make up one’s mind about something and then completely stop learning.
As I understand it Python is also strongly typed, while JavaScript is not.
By working with immutable structs and pattern matching, you can get many (but not all) of the same guarantees you would from static types.
For me, strong and dynamic is the worst possible combination. I actually need to be precise with types, but the language gives me no help.
I think Elixir has the best combination of runtime and tooling and ecosystem out there, and it’s type system is the only thing that prevents me from using it for nearly everything.
Now go and rewrite your dynamic language's runtime in a dynamic language.
https://github.com/robert-strandh/SICL (which I wrote a decent chunk of the compiler backend of.)
Sorbet [0] is close, just wish the syntax wasn't as verbose as it is.
Crystal Language [1] is even closer, I just wish it had better IDE support.
For me, full time Rubyist for over a decade, it was enough to "jump ship" to Rust and TypeScript. It's one more reason to forego Ruby, I fear.
We have projects in java7 still which span thousands of LoC and juniors can just jump in using their IDE and make some sort of contribution.
With python this takes much longer.
I’ve been through several typescript projects where ‘as unknown as any’ makes me wish we didn’t even have typescript in the first place.
If you can’t trust the type system it’s worse than not having one :/
"the language" is not enough per se, you have to use the tools it gives you to gain some advantage: those projects were clearly actively avoiding strong typing, for whatever reason
It's unfortunate that Typescript even provides this casting hack, though it's necessary for gradual, optional typing. The very first thing I would do on such a project is spend the time to fix those broken types, because it will pay back in full in productivity after a short time.
The term "strong typing" doesn't have a clear definition to begin with.
Other terms which it often substitutes do, e.g. static typing, sound type system, decidable type system etc.
e.g. https://perl.plover.com/yak/12views/samples/slide045.html
This seems to be a main reason certain people like it. It gives a sense of productivity. However I think it is misguided. It’s a false sense of productivity. Can they commit? Maybe, yes. Will it be right? Maybe, and more likely with types to handhold. But does it mean they understand the data model and the domain of the codebase they’re working in? No. No typing forces you to know what your code is actually doing.
It takes longer to be productive, but you’ll be productive because you know what you’re doing. Not because the IDE held your hand.
Widget factories or scrum farms will no doubt like the “ease” of jumping into a codebase that has typing, but I’m not yet convinced it’s better or that much better for the experience developer. I need to think on it more. For certain though I’ve seen enough in my time to know that how quickly a junior can “just jump in and make some sort of contribution” is not a good measurement.
You're 100% right though.
Of course abstractions can be misunderstood and misused, but is that an argument for not having them?
In fact, if all the developers change, it's a virtual certainty that none will ever understand the python code, while the odds are about even for java.
(But then, the types aren't the only factor for that.)
You are making a false dichotomy.
It's perfectly possible to work in a dynamic codebase without understanding the domain, business, logic or big picture. And it's just as perfectly possible to build a statically typed codebase that forces you to understand the whole entirety before being able to make a change.
Now, whether it's actuallygood to enforce true understanding of the Whole, before being able to work on a subset, is another debate. One that, unsurprisingly, has long been proven to be false. It's why we consider modules, functions, boundaries, coupling, classes, microservices, layers, and so fort and so on.
I'd tend to think the important feature here is strong typing, not static typing, and indeed TypeScript, which is hampered by the anemic type system of its parent JavaScript, it not the best example of strong typing.
Ultimately, strong typing is a hill I'm willing to die on, too. But a lot of people making arguments on this topic are conflating it with static typing, which just isn't the same thing. In general, that gives me the impression you haven't done enough work in enough different languages to really have an informed opinion. The type systems of different languages work within the context of the language as a whole, with tools like REPLs, test frameworks, constraint checkers, static analyzers, and IDEs carrying a lot of the work that might be attributed to a type system. There are a lot of confounding factors here, and if you're only talking about a few languages and you are making mistakes like conflating static=strong and dynamic=weak, you don't have the breadth of experience to understand what the benefits and downsides of strong types are.
Though uncommon, strong dynamic type systems do exist. Scheme is probably the most popular example, although its type system isn't the strongest. This is the approach I'm taking with Fur[1], which attempts to have as strong a type system as possible while still being dynamic. Python is... stronger than many other popular languages (i.e. JavaScript), but still does a lot of coercion (particularly around truth-y/false-y values--duck typing is a bit of a grey area in that it's sort of arguably weak typing, but also arguably indicates a capable type system rather than a weak type system).
But perhaps more critically, weak static type systems also exist, such as C, which is the source of many of the world's most serious bugs. It's highly unwise to assume that static typing is strong typing.
The more freedom we take away, the higher quality software we produce. Looking at you, Rust.
How are you measuring this?
All kidding aside, obviously I don't. That shouldn't detract from my statement at all though, nobody measures anything about software. We're all in the dark. Thats actually reinforcing my beliefs: we are in the dark because software is unmeasurable because of the freedoms taken by programmers that fail to abstract or fail to be formalized.
Formalizing software, and then starting to measure changes rigourously, is the first step we have to take towards becoming a mature industry.
Formalization is the oposite of freedom. People think software is like art, where freedom of expression broadens horizons. This is no longer true, most software is not art, most software is buttons that make money when clicked.
Artists live by constraints. Nobody prefer a blank canvas.
Abstract art enjoyes would like to have a word with you.
What's next, sit silently behind the piano for four and a half minutes and call it a musical performance??
Abstract art enjoyers would like to have a word with you.
Since the author is willing to die for it, they might want to know that they probably meant “dynamically typed”. Some dynamically typed languages are strong typed, too!
I don’t really see any arguments against dynamic typing in there.
Most strong, dynamically typed languages also have good developer tooling (elixir and dialyzer for example) and have built in ways of describing data structures.
"STRONG TYPING IS SO 90s!"
That missive stayed on the board for over a year. I still think about it from time to time and remember that the tech world's pendulum is always in motion.
Today? I adore TypeScript and I'm pretty excited about the PEP 695 QOL improvements to type annotations in Python 3.12. Where types and tools are concerned, we're in a much better place today than when Nirvana was still the next big thing.
But I won't be surprised at all if in another decade I see "STRONG TYPING IS SO 20s" marked on some corporate whiteboard in Redmond. :-)
The real discussion is not about types vs no-types, it's about the role and place for each of these bending of the rule and their associated tradeoff in different contexts. Op even takes inference for granted towards the end of the post, but make no mistake, that's a form of weak(er) typing.
For example, writing template code in C++ without frequent use of type inference (aka. auto) can be an absolute nightmare (I suspect that extends to generics programming at large, but C++ is where I have the most experience here), and the legibility gains from making use of it for iterators is broadly acknowledged as being easily worth the obfuscation for scope-limited variables. But there's also a strong case being made to avoid its use entirely in most other contexts.
Java by contrast has a greatly simplified generic system; Scala improves on that with robust type inference, making generics pretty trivial to write and use.
If you can't at least admit to the benefits of static types in 2023 then you probably don't have experience building or maintaining large software systems or working with a team of 15+ people, many of whom you've only met over Slack.
c# pretty much has that nailed. You can do dynamic typing if you want, but you're on your own if it fucks up. Moreover its obvious when you're doing dynamic silliness.
Python is strongly typed, the problem is that its also duck typed and everything is dynamic. Sure there is type hinting, but that's mostly optional and pretty much broken as its not really supported at run time. (unless I've missed something)
What I'd really like is something like perl's "use strict;" in python.
however I don't see that coming anytime soon.
Both styles require code to be disciplined, and undisciplined code cannot be caught by compilers, linters or automatic tools, it has to be through code review (or experience and sheer willpower). It's easy to think of examples where a lack of static types would allow brittle code to be written, but it's important to question whether that brittle code may be considered unacceptable by someone experienced in working without static types, in the same way that code using a Dictionary<string, object> instead of an actual type would be considered unacceptable by a type-safety practitioner despite being (technically) type-safe.
With dynamic typing, one would have only a small number of very different types, because having many types increases the risk of using the wrong one, and having similar types makes it harder to notice that the wrong one is being used. The types must be very simple, because deconstructing complex types requires having branches, and branches should be kept to a minimum because they are hard to cover properly with unit tests.
The inherent disarray of your unit tests must stem from the complexity of your algorithms, not from your types.
Second, it’s a shame that the author doesn’t really speak to Haskell at all. Knowing that a variable is a `u16` is rarely useful alone, relying on the meta of a variable name, for example, to prevent me from adding `user_age` and `number_of_legs`; they both are unsigned 16-bit `int`s, so the compiler is cool with it. A better type system allows types to hold semantic information beyond the bit-width or archaic and usually unhelpful information for the computer and not the developer. Relics of a past where 8k or RAM was all you could use for your program.
Ironically, the advanced Haskell type system looks more like dynamic typing specifically because the type inference is so good. You can rely on the compiler (or interpreter) to tell you what the type should be, often times allowing for much more flexible types than you’d think possible.
Also, being able to derive classes like Show and Read is amazing.
So, +1 for loving types!
You can still get runtime bugs, of course, but that kind of thing would manifest as a TypeError or something. All of those bugs could be caught with tests.
I like dynamically typed languages. They are simply the pragmatic choice for the vast majority of code that will ever be written. But being dynamic doesn't mean you can't do types. Python has optional type hinting and checkers like mypy are very good. Common Lisp has features that enable you to declare types and get near-native speed binaries.
These days I do type my Python code for many of the reasons given in the article, but I still enjoy using a dynamically typed language underneath.
That's more about type coercion which you can avoid in dynamic languages. See === vs == in JavaScript.
The post adds "static" typing in too, modifying the argument. Dynamically typed languages allow you to easily have a very intimate understanding of your program; because you can write the program and modify it while it's running using a REPL. The post seems to conflate dynamically typed with untyped, which is very much not the same thing. The two are actually incompatible ideas.
OP: I highly recommend you look into SICP if you want a concrete counter argument to static typing for large systems. A main focus of the course is controlling complexity through abstraction as systems grow to be large, and the course was taught using Scheme, which is a dynamically typed language.
Of course, the coin has a thin edge where the situation is a bit of heads and a bit of tails. Count me a fan of gradual typing.
My nuanced take is that typing is an economic choice. If the cost of failure (MTTR and criticality) are low enough it is fine to use dynamic typing. In fact, keeping the cost of failure low (if you can) gives you much more benefit than typing provides.
Erlang, a dynamic language used to create outrageously resilient systems, is a great example of that for the domains where it can be used.
I'm not a dynamic typing zealot (I like static typing a lot) but I do think that dynamic typing is unfairly maligned.
The cost argument brings the decision down to earth.
Java has had some type inference for almost 20 years, and local variable type inference (i.e. `var person1 = newPerson();`) for more than five.
Java, I'm sorry for swearing at you all these years. :P
You mean 11, right? 12 was not an LTS release, and has been out of support for years. The currently supported LTS releases are 8, 11, 17, and the recent 21 release.
Common Lisp seems to be the best of both worlds, with dynamic strong typing in general and advanced compiler type inference that can correctly point out mismatched types in most cases in SBCL, leading to much easier time programming interactively.
Try ML (OCaml, F#…) or Haskell, they have type inference. No need to declare types most of the time, but they’re there, checked at compile time.
If taken to an extreme then nearly all parameters can be made into unique types. Not just measures, but pixel height measures. Not just pixel height measures, but help panel pixel height measures. Not just help panel pixel height measures, but settings help panel pixel height measures and so on. This may seem like a great way to keep everything well sorted, but ends up being an unhelpful mess that is difficult to maintain. So the question is how exactly in a situation is best to define types, not whether or not to have them or to make them static or whatever else.
And why exactly do you need types in the first place? Lots of projects are messy and involve teams and have work handed off to developers on a regular basis. In that kind of context which is quite common types can be a life saver. There are also short term one person projects that have a high expectation of all work being thrown away soon after. Isn't defining a bunch of types likely to be a waste of time if there is only a brief development effort which is clear and not expected to ever be touched by teams?
Most one size all solutions like static typing have some contexts in which they are either a complete waste of time or need their breadth to be carefully limited.
10 years ago the bias was strongly the other way. And in many shops I've worked in, there was an intense hatred of having to declare types.
I like to think it's the development of better/smarter type systems in mainstream programming languages (vs say Java which is what was dominant then), as well as maybe maturing of software engineer practices.
But I also fear it's just the trend-pendulum ticking.
That and I notice a lot of Python jobs out there, which is an ecosystem I've kept away from for the last 10 years precisely because my professional experiences with large dynamic-typed late-bound Python codebases was terrifying. Beyond NumPy & ML related projects, I fear that this might be indicative that the "types are just getting in my way" crowd is still very influential...
Finally I also think it's important to distinguish two concept that get mixed up -- type vs binding: static typing vs dynamic typing and early-binding vs late-binding. It so happens that most languages that emphasize static typing emphasize early-binding, and vice-versa; but it's entirely possible to have very strong types but still introduce sloppy late binding decision behaviour that ends in all sorts of runtime exceptions. Java is/was notorious for this. Reading configuration files to drive application behaviour, or doing things like the Rust Axum framework's "axum::extract::State" stuff, which can blow up at runtime; they're not strictly type issues, but get mixed up in some of the things people are talking about here.
To be charitable, perhaps that the type system was getting in the way, physically of what they wanted to accomplish conceptually.
I actually think it has more to do with object-oriented programming as it was propagated through C++ and Java. We were told (and believed) that OO abstractions would solve many software complexity problems. The focus of a lot of programming work was/is on bringing these OO design patterns into play. In large part the OO thing is about a kind of dynamic dispatch. And yet languages like Java imposed a very rigid way of doing OO, and made some of the patterns awkward to express.
So if you believe in your heart in the promise of OO, but find that your OO experience is relatively handicapped by your static type system, one then blames the type system. Ruby was maybe the best example of where this line of thought was going/coming-from?
I think we are getting past this by getting (somewhat) past OO as a dominant design methodology. But also by adopting languages with richer static type systems.
A lot of the arguments against typing are a bit stale.
- We have transpilers now. Even most javascript coders use a transpiler or at least a minifier. The overhead of a type checker in such a tool chain is pretty minimal. Especially on a modern laptop.
- We have type inference and a few other modern compiler features now. Typing is a lot less verbose in e.g. Kotlin than it is in Java. Java now has a little bit of type inference but it is still pretty verbose in comparison. A lot of the typing is just added by tools as well.
- A lot of languages like python, ruby, and others have types now. And spin off languages that have better typing (e.g. Mojo, Crystal) and preserve most of the original languages perceived elegance. Just goes to show that typing can be done without too much compromises.
I'm very much a fan of strong static typing, but I understand the appeal of dynamic typing, particularly for small scripts and/or prototypes.
Static type checkers (for most languages) are pretty simple theorem provers. For statically typed languages, the compiler (or, I suppose in rare cases, interpreter) rejects any program that it can't prove is free of type errors. An alternative would be to only reject programs that provably contain type errors. The difference in allowed programs covers the cases where the type checker can't prove either way.
So, I'd love a language with a type checker that when ahead-of-time compiling code would refuse to emit libraries/binaries when it couldn't statically prove types were used correctly. In interpreted mode, it would only refuse to load code that it could statically prove contained type errors. Maybe when used within an interactive session, it would still allow (but warn) when code provably has type errors.
As an added bonus, static type guarantees open up more opportunities for the optimizer. A single language offering some range of static guarantees would reduce the number of cases where dynamically typed code gets re-written into a higher performance statically typed language when re-factored into libraries.
If you prefer, go ahead and dynamically type your scripts, etc. However, if/when it comes time to package up parts of your program for wider re-use as libraries, please tighten up the static guarantees of type safety.
Haskell can do that. https://downloads.haskell.org/~ghc/7.8.2/docs/html/users_gui...
All the TS devs just conflate JS with "no types" and picture drooling idiots that make random changes without knowing the type of anything (probably because that's what they do without 50 IDE popups to guide them). You're never going to convince them of the values of dynamic typing because they have a different mental model of it in their head.
It's like saying there's only one good kind of metal fastener for construction. Sometimes you should use nails, sometimes screws, sometimes bolts. It's not like one of those is always superior to the others. You literally should not use a bolt all the time, nor a screw, nor a nail. There are specific engineering requirements and purposes to them all and you can't just mix and match based on your personal preferences.
Well, let me correct myself: you could just use one fastener you prefer, but your building would fall apart six different ways and be a huge pain in the ass to construct. Thankfully we have building codes so people can't just decide to do that.
The article puts a few cases forward. What the article is discussing, however, is the majority of software. Built in varying teams. Over long periods of time. Large software, complex software.
I feel a moment of horror. Type coercion. In front of my very eyes! I quickly added proper conversion everywhere.
I have no problem with dynamic types, but type coercion is the devil.
Strong typing really shines when you have generics, saving bugs at one level above basic types..
I like the idea, feels very pythonic ironically. And I suppose if you don't want the generic, I imagine there is probably some subclass of that which is more restrictive.
Anyway, as a developer I love when people do this. As a developer this sounds like a nightmare to maintain. My coworkers have told me to limit the numbers of 'what ifs' and just roll with it.
Granted it's not always easy and straight forward but that what senior developers in the team are for..
https://docs.swift.org/swift-book/documentation/the-swift-pr...
Look at duck typing.
So he created spec. Spec can check more invariants than just typing. Truth to be told, Clojure is functional and one of the goals is to generate test cases from those specs. So it is a bit of a different story probably.
At the end we want correct software.
That's why you see a negative reaction to static typing. Its the developer experience. Its the error messages. Its the amount of time and mental effort it takes to type something correctly.
If you told me I had to choose between dynamic Python or strict Typescript I would choose Python. I love static typing. I hate Typescript.
So don't die on a hill! Just make better type systems!
To know which other files is using current file, i just add an additional property, then boom, the IDE go red on those files !
Without types, the best you can do is, search, search and search !
Static typing, maybe not so much.
@irrational Sorry for the plagiarism.
Over the years, I have done programming with C, C++, Java, C#, PHP, Python, OCaml, Lua, Nim, Golang, Rust, Objective-C, Flash, Bash, D, Scala, TypeScript, assembly, PL/SQL, F#, and a few that I am forgetting.
Compile time type checking can definitely make things easier.
But for dynamic languages that I have been programming in for many years to have types shoehorned into them as an afterthought feels like a kludge.
And it's a tradeoff. I got used to developing in Node or Python with vim without auto completion (although I had those things as a kid in IDEs for other languages and they can be great). And without compile time type checks.
You can pass JSON around and literally never do a database migration.
This stuff will lead to more runtime errors initially, but also means more streamlined code. And you have to do thorough testing regardless.
TypeScript might be a choice for a large project with several developers. But I would also argue that an even better choice would be just to use a different language that has the typing system you want built in rather than awkwardly bolted on.
What I see in most TypeScript projects is a half-assed attempt that still results in run time type errors, but with the added "benefit" of an awkward bolted on typing system and the need for more tools as well as losing any dynamic benefit.
They drop the idea of using JSON and make everything related to the database compile time checked if possible. I'm not saying that those checks can't be useful but if are going that route it would make more sense to just drop the dynamic language.
Having all of the dynamism can be a convenience and save you time and effort in other ways. But not if you treat it as a poor man's static language and shoehorn in types in a half assed way using a traditional database.
Part of this is that people think software engineering is about your choice of stack or adding processes to development.
What matters most is probably the feedback loop between the users and the developers. Starting with the requirements engineering. Second to that would be details of the software design and organization. Leveraging existing code effectively can be key. Using descriptive but relatively concise identifiers, short functions, good code organization.
But diving into and evaluating all of that stuff in detail takes effort and a lot of skill and so many make snap judgements based on surface level processes or tool selection.
If you have a truck with a 1000 liter tank on the back the type for the contents liter, or is it something liters_H2SO4 - if you only need to go to a gauge liter is good enough, but if you are doing anything else you do not want to mix the contents up with liters_H2O. Note that is is very likely you will want to have both in the same program in different areas! Of course we can take the tank out of the truck and replace it with a box of iron - so maybe we need an even more generic type that could be either liters or kg?
This talk does a great job at articulating how to use types to disallow invalid/illegal states.
But when I started working with python, I understood the beauty in it. At first I couldn't believe you could have a system running reliably that was being hit by millions of people around the world, but then that's when I fully appreciated the power of automated testing. I came from an enterprise shrinkwrap environment with 1.5 hr dev cycles, so you can forgive me for not understanding CD/CI at that point.
Yes, you can't detect when changes could cause bugs, but that's where testing comes in. Where I worked, we had to have complete code coverage so that every line of code was tested, and we needed to have a very strong automated testing. Using that method of testing, it made it a lot more predictable.
Sure, bugs can leak through, but having worked in C/C++ for so long, I can assure you that static typing only fixes a certain number of bugs, there are plenty more sources of bugs that just types.
So for the record, I like static typing and generally agree with using it, but I also see the beauty and simplicity in dynamic typing like Python and it's not complete chaos and there are ways to mitigate it.
I remember taking a badly written Python project which was difficult to work on because it took hours to run and could fail at the very end due to a syntax error or other trivial kind of mistake. I needed some way to make it less difficult to change so I could change it to something good. I tried applying type hints because I thought this had minor impact for potentially some gain. In this case it didn't and what helped was to take every tiny bit that was unit-testable and make a test and then to mock the part of the process that took all the time (it was an android build system so I mocked the android build). Every bit of refactoring-for-testability made it easier to do the next bit.
So to me I feel that writing tests has more value than any other activity - I'm ready to use types when it makes my IDE work better and when I think it stops me making a mistake I've made before but for my money tests deliver more and I will rather work on them than trying to build an elaborate type system that models the problem well enough to prevent one from doing invalid things under any circumstances. For me, the types/classes I create are there to make my program understandable and manageable as it gets bigger.
I find it useful to be able to choose where to put my effort. When I worked on Java long ago I found the type system of the JDK libraries horrendously overdesigned and hard to use - flexibility is sometimes bad if you don't gain from it but are forced to pay the price anyhow.
Replacing types with tests adds giant amounts of boiler-plate code, for little benefit.
Static typing from the beginning just sets me into a big design up front problem where I try to anticipate the flexibility that I will need and I start building lots of boilerplate abstractions that end up never being used or restricting me later.
I find it useful to write imperfect code with only the constraint that it is testable and then revise the structure and any types I need as it becomes evident that they are valuable in the context.
Only this toolchain will be used because it is the only one that makes sense. Everything will be so much easier if it we just did it in the functional style. Large programming projects can't exist without Agile (I've actually heard someone say this). J++! X Will Eat The World. No comments! Verbose comments! If we got the whole team on this one editor, imagine the synergy. Silverlight will take over the interactive web.
If types turn into a big deal and remain that way say, fifteen years from now, then it is probably a Good Idea But Not Strictly Necessary. Whatever it is people are so bound up about, it is likely that programming used to happen without it.
The only constants we have in programming are variables.
XP seemed to be the catalyst for the “code fast and bail” mentality that’s persisted for too long.
I feel somewhat validated as a long time advocate of type definitions.
It always irked me to hear someone say a language with types is too “verbose” and slows them down.
Yeah, please do slow down! It really is okay to take a little more time to write better code.
We can and do! Use SML!
People just don't find out about the good type systems because the market is crowded out by mainstream shit.
And people write pieces about how good this shit is: "I was going to add an Int to a String but the compiler stopped me! Less [sic] bugs!". With arguments like that, why would you switch camps?
Or sometimes you get fed shit and get told it's inference: "I can replace the first word on the line with 'var'!".
So what are the downsides?
Inference doesn't play well with inheritance or mutability.
If you read Effective Java and see the points about avoiding mutation and inheritance, then these downsides may very well seem like upsides to you.
Why did you stop coding in SML?
SML uses Hindley-Milner (with some modifications to support imperative code). Many statically typed functional languages descend from it: Haskell and OCaml are the most prominent.
> Other languages haven't adopted a similar system so there must be some kind of trade off. What was the downside?
If you're asking why existing popular languages don't adopt it: they can't. HM type inference requires a HM type system, and they've committed to a different one. At best you could get good inference on a HM-typed fragment within the language (though even that would mean making a lot of painful tradeoffs) but it would break down the moment it made contact with existing code.
If you're asking why HM languages didn't become more popular? Historical accident, in my opinion. I don't think it has much to do with the type system, given how fundamentally awful Java's is.
If you have strong static typing you don't need all those dumb functions like string_to_number("1234"), etc. to change the type you're given to the type you find you need right now.
When you know that a variable is of a set type, you DO NOT need to keep looking to see what type it happens to be at the current moment, and then juggling it to suit.
"Aha!" you say, "but by enforcing conditional checks at variable definition and inside function signatures we don't have to check them ourselves in our code and the compiler itself can check them to ensure correctness and optimize performance!"
...and I'd agree with that sentiment but I'd also add that by having to reason about your types constantly you're adding a non-trivial amount of mental overhead when structuring your code. For some people that's how they reason about their code anyway but for others, having to consider whether or not a number is a `u8`, `u32`, or `usize` adds a lot of complexity to the code that could very well be entirely unnecessary depending on the use case.
Consider, for example a program that's meant to be executed on the command line that takes two numbers and adds them together. No matter what language you use you have to convert the raw string input into a numerical type before performing the addition.
In Python you have a simple function, `int()` that will happily turn any string consisting of numbers into a proper integer that can be used for math operations. If it gets something that isn't a number it'll throw an exception which is trivially easy to detect and wrap in a `try`/`except` block with a user-friendly error message. If we wanted to support floating point numbers we could just use `float()` and the code would otherwise be exactly the same.
In strongly-and-statically-typed languages this same process involves considerably more overhead. Before we can select a function to convert a string into a number we must first pick a numerical type and before we can do that we must think real hard about how big of a number we're going to support in this application. Our function signature(s) could also get complicated because, "wait: what type of string are we going to get from the command line?" Something as simple as adding two numbers becomes complicated really quickly!
Comparing the two ways of handling things (strongly, statically typed VS strongly, dynamically typed), Python ends up the clear winner a lot of the time because you got the job done without having to think as much, with less code that's super easy to reason about even though it didn't use static typing.
The other problem is that when you write such trivial programs in strongly, statically-typed languages like C, C++, Rust, etc it requires a lot more mental overhead to look at the code to figure out what's going on/how it works (though with Rust, less so because there's not 1001 awful ways to do things haha).
Except this has literally nothing to do with static typing and everything to do with whether or not the language you're using cares about numeric types.
For instance in typescript you'd just have `number` as the type. You could conceive of a dynamically typed language like python where adding a u8 to a u32 would cause a runtime error. In which case it sure would be nice to have the compiler tell you to convert before you called a function.
>The other problem is that when you write such trivial programs
If only I had a career writing trivial programs that would make a lot of things easier.
For starters this is not the case, it does not work on a string of numbers:
>>> int("123 890 123")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '123 890 123'
> Before we can select a function to convert a string into a number we must first pick a numerical type and before we can do that we must think real hard about how big of a number we're going to support in this applicationAren't you doing the exact same thing in Python by picking `int()` over `float()`?
> what type of string are we going to get from the command line?
Well, this might be the case for system languages, but most have a single `String` type.
Here is the program you described in Python
def main():
n1 = input()
n2 = input()
sum = int(n1) + int(n2)
print(sum)
And here it's in Haskell main = do
n1 <- getLine
n2 <- getLine
let sum = read n1 + read n2
print sum
Accepting a single line of numbers is not much different main = do
nums <- words <$> getLine
let res = sum $ map read nums
print res
I didn't think much about it, it's not a lot of code and I guess it's easy to reason about?But there's one cost that the author seems to miss and that I didn't fully appreciate for some time. That's the cost of composition. If you look at python you can pretty much compose all possible packages on pypi in your project. All their means of abstraction are usually fully compatible (except for concurrency). Composition often only requires a little bit of glue code.
In a statically typed language's ecosystem you will probably a handful of fundamentally different means of abstraction like functors vs. classes that simply don't compose well together. Hence the fragmentation increases massively.
I think nushell disproves this example :)
Probably the biggest issue with python.
Seemingly random changes between float64 and object happen when doing unit testing(pandas).
Can't remember the exact situation because I switched to While permanently, but there was some For loop problem. If the data came in as length 1, the for loop would read the individual chars of the string. If it came in as >1 it would iterate through the list.
15 years ago when I started python, maybe it made sense. Probably ~5-6 'oof that was a bug I wasted time' in 3 years professional use. Today, I find myself forcing types to solve a bug, or at a minimum checking the types as the program progresses.
What you call flexibility I call non-readability:
df.ix[0]
df.loc["x"]
df.loc["x", :]
df.iloc[0]
df.x
df.x[0]
df["x"]
df[["x"]]
df[df["x"]]
df["x", 0]
df["x"][0]
df[:, 0]
# and probably a few more I forgot
If you stop working with it for 2 months you forget what each one is doing and when you are supposed to use it.Also, note that pandas's datafrimes shine when you focus on column operations, if you're finding yourself accessing individual elements by index often, that's usually a sign that you should switch to a more appropriate data structure.
That when you look at online examples or other people's code you will find all of these variants used.
Sure, doing an operation in Polars can be more verbose, but it really does have just a few ways of doing things. Which in the end is actually more productive, since you don't need to always google the syntax.
You probably did this:
for name in ("John"):
# "J"
# "o"
# "h"
# "n"
for name in ("John", "Sam"):
# "John"
# "Sam"
The correct form for the first one is this: for name in ("John",):
# "John"Most of my findings around wasted time and effort or correctness stem from design (faulty or skipping design analysis altogether and just coding). Every other aspect of coding just pales into insignificance compared to this one thing.
Once you reach a certain size of developers, add static typing if available, e.g. TypeScript, Sorbet, etc
That last part is the catch, and I've seen plenty of people think they're using it well when they're actually using it very, very, very poorly.
Dynamic typing isn't just a convenience sometimes, sometimes it helps compensate for poorly planned type structures.
Like a lot of things, there are tradeoffs in terms of what types of errors you want to avoid more.
Wait, I've never looked at it. I guess that's why I used the word trust. Surely it confirms my preferences.
I learned programming with types (C++). Was then beaten over the head that types didn't really matter/yolo. And now we're back here again.
It could have always been this way. And for some, I'd imagine the type party never took a break.
Now, not a lot of developers are really doing this. But it's still a good reason for those who are.
Under a properly modern static typing system, you let that be inferred:
let var = cache.get("key");
var is of whatever type that the cache object returns. Even clunky old C++ can do this with auto.That author will most likely die in some statically statically typed corner he painted himself into.
The best mix for me is to lean heavily on the type system and only lightly on tests, trying to write code that is simple and "obviously correct". Obviously-correct code has already had its problems ironed out in the first 6 or so bullets below, and there is very little room for further bugs to hide. Of course even "obviously correct code" can actually have bugs or be subtly incorrect -- a better definition is that any surprises in such code would also be surprises to the tests. The tests are nothing more than mirror images of the code, and therefore are not helpful or necessary. Static typing helps me think about the problem clearly, and helps me write "obviously correct" code.
Here's my working list, in order of preference of when I'd like to find the problem. Every time you drop down this list, the cost to fix the problem gets multiplied by some small factor ~1.5 to 3.
- Initial ideation ["Facebook for dogs? That idea sucks."]
- Requirements analysis ["That's not actually how we calculate that metric."]
- Conceptual system design ["Wait, these parts don't fit together like that."]
- Low-level design ["Crap, a loop won't work here, I need a different flow."]
- Brain-to-fingers typing ["Whoops I almost typed 'elesif'"]
- Immediately post-typing [Red squiggly line under 'elesif']
- Re-reading ["Wait that should be i-1, not i"]
- Compile time ["SYNTAX ERROR: Unknown identifier 'elesif'"]
- Unit test time ["Assertion failed: expected 7, got 8"]
- Code review time ["You didn't handle the case where N is negative"]
- Merge / integration test time ["Oh crap, David's commit broke my commit! How the hell do I merge this?"]
- Internal manual testing time ["You need to tighten up the graphics on level 3"]
- Production ["The users say the application keeps crashing when they run this report! Fix it!"]
- Years later/never ["It turns out that the analysis code we've used for the last 10 years had a major bug in it the whole time and we've been giving wrong numbers to a regulatory agency that has the power to shut down our company."]
Citation needed.
This is ENORMOUSLY context dependent. Enough to invalidate the entire point.
My day job is writing code that runs on a server I control. Mistakes there are super cheap to fix. I get a nice crash in sentry with stack and variables for all frames, I fix it, deploy. Done.
If I was shipping code to Mars, well that's the opposite situation.
CONTEXT MATTERS
> Citation needed.
Really? Isn't this one of the most clear and repeatedly demonstrated facts in all of software engineering? We don't have a whole lot of clear-cut demonstrable facts in this industry, but this is absolutely one of them.
How about this 2002 NIST report, Table 1-5 on page 1-13 is "Relative Costs to Repair Defects when Found at Different Stages of the Life-Cycle" and shows 1X at "Requirements", 90X at "System Testing", 440X at "acceptance testing", and up to 880X at "operation and maintenance".
(Warning: 300-page PDF)
https://www.nist.gov/system/files/documents/director/plannin...
You can find thousands of people saying the same thing from their personal experience with a quick Google. On the list of uncontroversial statements about software engineering, this is right at the top. Is our discipline so immature that we can't even agree on this extremely basic fact?
Another (okay, they don't say "exponential" here, or try to quantify it at all):
> The cost of finding and fixing bugs or defects is the largest single expense element in the software lifecycle. ... The earlier in the development lifecycle defects are found, the more economical the overall delivery will be.
The Cost of Poor Software Quality in the US: A 2020 Report, Consortium for Information & Software Quality
https://www.it-cisq.org/cisq-files/pdf/CPSQ-2020-report.pdf
> My day job is writing code that runs on a server I control. Mistakes there are super cheap to fix.
That's good to hear, but mistakes caught before they get to your server are even cheaper still. Perhaps your exponential factors are smaller than NASA's, but it still clearly costs you much more to fix a problem that made it to your production server (and possibly impacted your users) than to fix it immediately after typing it because your IDE showed you a red squiggly line.
It's a repeated saying.
> You can find thousands of people saying the same thing from their personal experience with a quick Google.
I know.
> That's good to hear, but mistakes caught before they get to your server are even cheaper still
That's a nonsense statement. If they are caught AT NO COST they are of course cheaper yea. But that's the thing. It's all tradeoffs. Everything is about tradeoffs. What is the marginal cost of finding bugs?
> than to fix it immediately after typing it because your IDE showed you a red squiggly line.
Yea, that's nice. In that case the marginal cost is approaching zero. But how many percentage of all bugs is that? And what is the cost?
How many bugs will my IDE find if I switch from Python to Haskell or Rust? And how will that impact my development speed?
I write my frontend in Elm because that's code I ship to users (via the browser) and THERE the costs are enormously worse, often because the problems can be seen by customers and not internal users (again: CONTEXT). I'm willing to tolerate a big productivity cut for being sure the code is correct there. But on my server? Nope. I am not willing to leave an easy order of magnitude of development time on the table for that type of safety.
And why stop at static typing? Why don't you run ALL your code through Property Based Testing AND Mutation Testing 100% before shipping? What's the marginal cost of catching a bug then? Why not run ALL your code through a theory prover like Coq?
Bugs are NOT always "cheaper" when found early. It's about the COST of finding the bug early. Of course it is! If the cost is a trillion dollars in programmer time to find a bug that takes 1 second to fix in production at a cost of a few dollars of programmer time, by definition it was NOT worth it.
It's ALWAYS a tradeoff. Of course it is. Everything is.
It's maddening to me that people refuse to see this when it's so blatantly obvious.
> the marginal cost [of fixing a red squiggly] is approaching zero. But how many percentage of all bugs is that? And what is the cost?
The marginal cost of fixing a red squiggly underline is indeed basically zero for typos, and typos are a class of bugs I'd like my static type system to help me catch; and it does, and dynamic type systems generally don't. So the percentage of typos that become bugs is basically zero, but that's partly because of static typing! When you say "the percentage of all bugs", your definition of "bug" is a-priori excluding things that were caught before they became bugs!
But you can also get a red squiggly underline because you thought some variable was in scope when it's actually not -- that bug could have been caught earlier. The cost of fixing that bug is not near zero: the structure of your program is different between your brain and your code. The easiest bug to fix is the one you never write in the first place.
Or you get a red squiggly because you thought you could pass an object of type X into a method you're relying on, but you actually can't. Dynamic typing will not help you with this, and the cost of fixing that bug is not zero: again, your assumptions about how your program is fitting together are incorrect. I'd much rather realize that I can't use a method in exactly the way I thought right at the moment of typing it than later during test time. The latter requires writing and maintaining a test in order to catch a problem late that I could have caught early with no test.
> And why stop at static typing? Why don't you run ALL your code through Property Based Testing AND Mutation Testing 100% before shipping?
I directly addressed the subject of testing in my original post. Static typing allows me to write many fewer tests than dynamic typing.
> If the cost is a trillion dollars in programmer time to find a bug that takes 1 second to fix in production at a cost of a few dollars of programmer time, by definition it was NOT worth it.
I just don't understand this hypothetical. Why would it ever take a trillion dollars to prevent a bug that takes 1 second to fix in production? This just doesn't jive with any developer experience I've ever heard about, any example I've ever read about, or anything I can even imagine.
You seem to be operating under the assumption that introducing static typing makes you slower at programming, but we're claiming it's worth it. No, that's not my claim. My claim is that above some level of program complexity, static typing makes you strictly faster at programming, full stop. That level of program complexity may not be your internal server or whatever, but it definitely is most production-grade software out there.
> It's ALWAYS a tradeoff. Of course it is. Everything is. It's maddening to me that people refuse to see this when it's so blatantly obvious.
This is generally true in engineering, but not always. It is actually possible to simply advance the discipline. When you're building a suspension bridge, there's a tradeoff between Stainless Steel and High-Carbon Steel, but there's not a tradeoff between Steel and Popsicle Sticks. Unless, of course, you're building a toy or a proof of concept. Dynamic typing is fine for toys and proofs of concepts; nobody is disputing that. Professional engineers don't spend most of their time discussing the best way to build toys.
I don't see how static typing helps with "solving the wrong problem" kind of issues at all. That's what asking deep questions about the business is for. Maaaaybe DDD can do something here, but that's hardly "static typing".
> I directly addressed the subject of testing in my original post. Static typing allows me to write many fewer tests than dynamic typing.
I noticed here that you didn't make any difference between testing, PBT, MT and proof systems. That's pretty bad. I won't conflate Haskell-style good type systems with C-style horrible type systems, so you shouldn't do the same with testing.
One example is F#'s unit of measure system, which can statically verify dimensional agreement of arithmetic. Another example would be differentiating between "Safe Strings" and "Unsafe Strings" (say, untrusted user input) and statically enforcing which are allowed in which method. They won't tell you that "Facebook for Dogs" is a bad idea, but they may reveal subject-matter errors in the actual requirements themselves. Most test suites wouldn't even catch these errors, because test assertions are generally based on requirements. You could write your own unit-verifier that you run in tests, but then you're just reinventing static typing. In fact it's a common criticism of test suites in dynamically typed languages that they end up poorly reinventing checkers for the guarantees already provided by a decent static type checker.
> I noticed here that you didn't make any difference between testing, PBT, MT and proof systems
I also didn't differentiate between the Late Middle Ages and the Early Renaissance, because it was off topic. But if you insist:
"Property-Based Testing" is a testing pattern, not a different kind of test. Write tests and assertions as appropriate for the thing you're testing. It seems to me that you should have enough understanding of the likely failure scenarios and edge cases of your code that you don't need to be randomly generating inputs, but I can see how it'd be useful. I test all sorts of invariants in my tests. This just doesn't seem like a novel thing to me.
"Mutation Testing" is basically testing your testing methodology. Okay. You could also test your bus factor by randomly shutting out one developer for a day each month and seeing how you get on. Maybe good ideas for some companies? If you find yourself trying to develop a completely airtight testing process that will catch 100% of bugs, it's probably because too many bugs are passing your tests. Before you kill yourselves trying to develop an airtight, mathematically-guaranteed strong test system, may I suggest strong static type systems instead? Just let the compiler do it.
"Proof systems" are strong static type systems.
The latter. Other question?
Beyond initial prototyping (which I doubt will actually ever be thrown away), what logical defence is there for catching obvious bugs in production when they could have been caught at compile time instead? Dynamic typing straight up hides unavoidable realities of programming - you have to know what thing you have to cover all the cases. I don't just mean cases that "make the compiler happy". These cases are most often not mutually exclusive from the business domain - cases related to core business logic are missed, which produce more bugs which makes your software objectively worse for both the end user and whatever business the software is sold by.
The complaints I hear are that it's too cumbersome or annoying to satisfy these cases - my retort is to stop complaining because most of us are paid to write working software, not software we can pass off as being complete when it's not.
And don't suggest that tests cover this - they absolutely don't, and testing such things is a total waste of time. The train has left the station, best get aboard.
Even the article we're discussing points out there are cases for no- or dynamic typing.
All I'm pointing out, is that people who oppose strong static typing on the base of "but we don't want it everywhere" are drawing the wrong conclusions, because, indeed, we don't want it everywhere. We (I) do, however, need and want it in far more places than it's used today.
https://news.ycombinator.com/item?id=32806179
It's certainly not proven a static language can't cover this use case but in other areas static languages really have proven their merit.
You can either hire 10 dynamic typing software developers or 30 static typing software developers. Businesses which choose to hire 30 static typing software developers will fall to the companies which hired 10 dynamic typing software developers. It's pure economics.
Static typing fills your codebase with bugs. Whilst an inexperienced software developer might not understand why static typing increases rather than decreases bugs, you can see the increase in bugs in research papers on the topic. As plan as day.
(Hint: You have made your code more complex by using static typing and more complex code contains more bugs.)
I’ve written a number of Turing machine programs; yeah it’s impressive that an imaginary machine that is so simple can run any program that runs on any other computer, but it’s no fun writing code for a Turing machine. For the Theory of Computing studying Turing machines is important.
If I don’t want Turing complete type systems, what do I want? Well, first off, the bugs I find in my programs that are more troubling don’t seem to be stopped by strong type systems. What I want is to be able to declare assertions in my code. For example, I’d like to tell the compiler that the variable year must always satisfy (2000 < year < 3000) and that in a certain block of code that x and y are always within 0.5 of each other. I want the compiler to ensure that these invariants are satisfied by a combination of compile time and run time checks generated by the compiler.
The problem is that we can’t expect programmers to use Turing machine like type declarations, because the programmer must now program the type system without bugs to ensure that the types in the program carry enough information that higher level assertions can be made about the original program’s correctness.
TLDR, I want to work with higher level assertions about the code than that provided by complex types.
> A Turing machine is not "so simple that it can run any any program ...".
Simplicity has nothing to do with it. Turing showed that some very simple machines can calculate recursive functions.
Not every machine that is Turing Complete looks like a tape machine from Turing's papers.
> it's no fun writing code for a Turing machine
Almost every general-purpose programming language is a Universal Turing Machine; so that's like saying it's not fun to write code, period.
let person1 = new_person();
i prefer the c++ way (which also supports type inference):
Person person1;
Zealotry and fucking bullshit.
edit: To clarify, I am on the "types good" side of things
And then, kind of orthogonal to that, there’s also static typing and dynamic typing.
I think strong, dynamic typing is just as good as strong static typing (weak typing is objectively bad)
As long as the types of your variables don’t change from under you, that’s good, but I’m also okay with the compiler / runtime figuring out what those types are for me.
A side (but many would also say critical) benefit of types is that they act as a form of documentation that can never go stale (because they are enforced by the compiler). "Dynamic typing" does not offer this benefit whatsoever.
JS will convert types under your butt if you're not careful, Python won't.
Sure, static typing (when done well) is superior to that, but the title talks about strong typing.
That’s the crux of why I’m not all in on static typing all the time. (Especially for networked programs that expect a wide range of different kinds of input)
Having to prescribe the types I need before I actually need them goes against how I tend to build.
Almost all static type systems have escape hatches that let you go dynamic when you really really need it. Also, I bet that most of your use cases for dynamic typing could be solved with a simple tagged union. Most people bemoaning the lack of flexibility of static typing just don’t know about how tagged unions can easily emulate dynamic typing when we need it, such that we rarely even need to reach for the actual escape hatches.
> (it’s impossible to know exactly what data structure you need at the start of a project)
Thankfully data structures are even easier to change with static typing: change it, gets a ton of type errors, fix them, done. With dynamic typing you run the risk of missing a call site.
> Having to prescribe the types I need before I actually need them goes against how I tend to build.
There’s type inference for that. I personally take advantage of it any chance I get.
In Common Lisp, it's common practice to not accept code unless all such warnings are gone.
Every argument I’ve seen in favour of dynamic typing is some variation of “I like it this way.” They’re not technical arguments because dynamic typing is a strict subset of a statically typed language, equivalent to passing a flag to turn the type checker off.
Sure, not every compiler offers such a flag, but that is an argument against one particular static language (or group of languages), not an argument against static types itself.
Types are not exclusively for verification, and even if you decide it is, you can do a lot more with a type error than exiting the program.
That stance most people keep repeating is actually ridiculous. The most common usage of static type systems is to verify badly-written ad-hock dynamic ones that handle user errors.
This is just incorrect. Plenty, if not most, dynamically-typed languages are compiled. Indeed even the idea that there's a clean dichotomy between compiled and interpreted is an outdated idea: I'm not aware of any production-quality language implementations which run tree-walk interpreters entirely without compilation. Modern "interpreters" typically compile to bytecode but in some cases even can compile to native code, they simply do so in a just-in-time manner. These compilation steps are quite capable of applying type systems.
For example, if you run a Python script, you can see the results of compilation cached in .pyc files (these will be either in the same directory as the source files, or in your __pycache__ folder, depending on your configuration).
> The whole point of types is to be able to check things without running the program, because exercising every possible code path becomes increasingly untenable at scale.
Is it? I would argue that the point of types is to report errors at the place where they occur, rather than doing the wrong thing silently and reporting an error elsewhere, or simply behaving incorrectly.
If a tree falls in a forest and nobody hears it fall, does it really fall at all? If a bug happens on a code path, and nobody exercises that code path, is there really a bug?
I would never want a statically typed, compiled awk for example.
I generally think large, production, multi-developer software should be written in a statically typed, compiled language.
Strong vs Weak typing is a closer to a debate about having a name spacing system or not. There is really no great reason to have weak typing or not use a name spacing system.
Mainstream dynamically typed languages are moving in statically typed direction. Python (Mypy, Pyright and others), Ruby (Sorbent, Rbs), JavaScript (Typescript, Flow). How many statically typed languages optionally removed types?
Personally I think Python moving in the direction of static typing is a mistake, dynamic typing is very useful for domains where python is strongest: modeling, statistics, scientific computing etc. It's also part of the basic design of Python to be a dynamic language. Likewise Ruby, with it's heavy use of metaprogramming, also benefits tremendously from a lack of types.
But let me be clear: I do think statically typed languages are a very good idea for large production systems, I just personally do a lot of programming that's not for these systems.
> How many statically typed languages optionally removed types?
I wouldn't say "removed" types, but I've been in software along enough to remember when dynamic typing was the big hot thing and crusty old Java devs complained that we couldn't possibly live without static type annotations. I distinctly remember when C# introduced `var` (which is of course really type inference, not dynamic typing) to appeal to devs that were growing weary of types.
There's a great example in SICP of implementing a full object system in just a few lines of code that would not be possible to implement as elegantly in a statically typed language. Do I want that for a production system? No. But there is, or at least used to be, a world of computation being done for reasons other that quickly getting PRs pushed out to prod.
> I can see both side of the arguments on many topics, such as vim vs. emacs, tabs vs. spaces, and even much more controversial ones. Though in this case, the costs are so low compared to the benefits that I just don't understand why anyone would ever choose not to use types.
> I'd love to know what I'm missing, but until then: Strong typing is a hill I'm willing to die on.
I genuinely want to know what I'm missing. I also outlined in the post all of the arguments I usually see in favor of not having types and why I disagree with them.
I'm happy, willing, and excited to hear what I'm missing.
Is there a language out there that gives you that choice?
I expect you mean choose not to use strong static typing as per the original piece? Compatibility is a pretty good reason. I'd like to see Javascript die in the fiery pits of hell as much as the next guy, but its positioning means it is almost inevitable that some system will make it the only reasonable choice for you to choose if you want to build for that system.
Typescript doesn't help. It adds static typing, but not strong typing.
Usually: A strong type system does not allow types to change after being established. A weak type system allows types to change. This is also described in the original article.
so... like in c++?
Generally "strong" vs "weak" typing[1] is an ill-defined an mostly useless definition.
[1] as opposed to static vs dynamic or safe vs unsafe.
edit: I guess what you want to say is that implicit casts make a type system weak. But even there there is plenty of wiggle room: I think that everybody agree that implicitly converting the string "1" to an integer is bad (which is not allowed in C++). Narrowing conversions are arguably bad (they are sometimes allowed in C++), but some other implicit conversions are hard to argue against (int to long for example, or derived to base).
For dynamic types, sure, that's a reasonable enough definition. But it doesn't really make sense for static types: they're attached to expressions in your source code, not runtime values.
In order to have that, you need static typing of variables and constants, like most statically typed imperative languages have. But you also need a method to specify required input types and expected output types of all functions.
In a purely functional language like Haskell where there are only functions, and functions are first class and can be both inputs to or outputs from other functions, then the entire operation of the program is encapsulated in its function type signatures.
The entire flow of data and logic through the program can be type-checked by the compiler, and function implementations checked against their type signatures.
Or do you mean that the C-based escape hatches like casting pointers make the type system inherently weak? You don't have to use them though...
Said UB is observationally indistinguishable from miscompilation under some toolchains under some optimisation controls.
It also has various bolt on weirdness like const doesn't mean the thing won't be changed by some other pointer so you can't constant propagate based on it, unless it's written on the global, at which point attempts to mutate it anyway may succeed under the usual UB challenges.
Maybe that's a "strong" type system, but you'd only define it like that if you started by taking C++ as axiomatically reasonable.
> I'd like to see Javascript die in the fiery pits of hell as much as the next guy
Part of the problem is that some of those "next guys" don't have the experience and/or vision to realize that it needs to die. Perhaps we should emulate Cato in our subsequent HN posts:
And furthermore, I consider it necessary that Javascript be replaced with WebAssembly so that we can use well-designed languages in the browser.
As soon as there's any complexity at all or another person involved that exception stops.
Now, in general, I agree with you. If it's a one-off (or if it's really never going to need maintenance), and if it's small enough that you don't need types while writing it, then sure, do whatever is easiest at the time. But "never going to need maintenance" often turns out to be a lie, and when that time comes, you may be happy for some types as signposts to give a hint of what you were thinking all those months or years ago.
Otherwise, if you're unable to source another script, you could just have another script return a normal result in addition to a JSON blob of the environment variable changes the caller should make, which is probably worse than just allowing sourcing.
Pipes allow arbitrarily complex networks of communicating sequential processes. In some cases, networks of tiny CSPs are cleaner, but without discipline, they can rapidly become worse than huge monoliths.
Particularly as programs start out simple and organically grow, once they start hitting length limits, they're going to start evolving into locally-distributed computation. Maybe you get something nice like composable small unix-like utilities. However, I suspect there's a large overlap between the set of developers who would do that well and the set of developers who would still keep things nice and modular within a single process if they didn't have size limits.
Also, type weakness is more a property of the runtime than the language, but for those languages with specifications, the language specification usually also specifies a large amount of behavior for a compliant runtime.
The C runtime, will gladly (with UB nasal demon caveats) allow you to treat an int as a pointer. There are static type checks, but no hard limits on the type-confused nonsense it will attempt to execute. C is statically typed, but weakly typed.
Java, C#, etc. are strong statically typed. There are static checks at compile time, and the runtimes do their best (modulo escape hatches) to use dynamic runtime type checks to plug the gaps in the static type systems. (For any sufficiently complex sound/consistent type system in a Turing-complete language, as per Godel's incompleteness theorem and the halting problem, there will be some programs that cannot statically type-check but still will never encounter a type error, regardless of input. So, in practice, there will always be escape hatches in the type system to allow programs that are correct but not provable. Hopefully most of these escape hatches are backed by dynamic runtime checks.)
Of course, these categories are a partially-ordered continuum, not clear binary distinctions. For some pairs of languages, you can say one's statically enforced properties are a superset of the other or that one runtime enforces a strict superset of the other's dynamic checks. However, it's often a case that you can't say one language strictly offers stronger static type guarantees or one runtime strictly enforces stronger dynamic type restrictions.
What Lisp are you using that doesn't have types?
The vast majority yes, that's why I was a bit confused :D
There some languages that have no types. The only thing I can think of though is a POSIX shell minus arrays. (Edit: Assembly and Forth are two better examples)
> Most Lisps do not have a static type system
True
> and even fewer have a "strong" type system.
Common Lisp, Emacs Lisp, Scheme, Hylang, Clojure, and Racket all feature strong typing. I'm curious where you have found this trove of weakly typed Lisp dialects
I think that interpretation of the "strong" vs "weak" scale is a valuable one within the context of the blog post. The post is at least partially about how type systems can help the programmer by making them aware of certain kinds of errors (it is also about when: static vs dynamic).
My understanding of the terms covariance and contravariance are a bit shaky. Could you provide an example in another language that you think cannot be expressed using the provided utilities of most Lisp's?
You also mentioned that you don't think most Lisp's have "expressive" type systems. What do you mean by that? When I think of a type system as being expressive, I think of it as having explicit rather than implicit types, which is unrelated to the issue of strongly vs weakly typed and static vs dynamic types. Do you mean more like how you can describe / constrain the relationships between types in certain strongly typed languages?
Sure, agree. That's also the real point: type systems primarily exist to make semantically impossible computations unrepresentable in the language (at least, without some extra song and dance). To this end, Lisps have rather lackluster type systems, but they don't try to encode much of the languages semantics into a type system.
> My understanding of the terms covariance and contravariance are a bit shaky. Could you provide an example in another language that you think cannot be expressed using the provided utilities of most Lisp's?
The wikipedia article on the topic is a great source: https://en.wikipedia.org/wiki/Covariance_and_contravariance_...
I'm actually mostly concerned with type invariants (ex. List[int]), which don't really have much for representation in Lisps from what I've seen. Further, see above about using types to make compile-time assertions/checks about the behavior at runtime.
> You also mentioned that you don't think most Lisp's have "expressive" type systems. What do you mean by that? When I think of a type system as being expressive, I think of it as having explicit rather than implicit types, which is unrelated to the issue of strongly vs weakly typed and static vs dynamic types. Do you mean more like how you can describe / constrain the relationships between types in certain strongly typed languages?
"Expressive" is a nothing word that doesn't have concrete meaning in-context, similar to "strong" type system. That being said, I would say Rust, OCaml, and TypeScript have expressive type systems: the behavior of the language is largely encoded as types. The implicit vs explicit nature of types is not super consequential IMO, it has more to do with how you primarily represent semantic meaning. In lisp, it's symbols. In Rust, it's traits, enums, and structs (+ the affine types, but that's not relevant here).
That makes inheritance-based covariance and contravariance largely moot.
You have substitutability-based covariance and contravariance (you can't get away from those) but they cannot be subdued declaratively.
E.g. if you're passing a callback function somewhere, which you know will pass widget objects to the callback, it's okay to use a function that was written to handle gadget objects, if widget objects are designed to substitute for gadget objects.
That's contravariance of substitutability.
Substitutability is the only thing that matters in the end. Declared inheritance doesn't ipso facto guarantee substitutability, and therefore declared covariance or contravariance, which are inheritance based, do not guarantee actual substitutability-based covariance or contravariance.
Having an order of magnitude more unit tests is another option, but that undermines the alleged "less code" benefit of weak typing.
In [0] in particular there're slides 22/23, here is part of the transcript but makes more sense with the slides on:
> And you can call them problems, and I'm going to call them the problems of programming. And I've ordered them here [...] I've ordered them here in terms of severity. And severity manifests itself in a couple of ways. Most important, cost. What's the cost of getting this wrong? At the very top you have the domain complexity, about which you could do nothing. This is just the world. It's as complex as it is. > > But the very next level is the where we start programming, right? We look at the world and say, "I've got an idea about how this is and how it's supposed to be and how, you know, my program can be effective about addressing it". And the problem is, if you don't have a good idea about how the world is, or you can't map that well to a solution, everything downstream from that is going to fail. There's no surviving this misconception problem. And the cost of dealing with misconceptions is incredibly high. > >So this is 10x, a full order of magnitude reduction in (?) severity before we get to the set of problems I think are more in the domain of what programming languages can help with, right? And because you can read these they'll all going to come up in a second as I go through each one on some slide so I'm not going to read them all out right now. But importantly there's another break where we get to trivialisms of problems in programming. Like typos and just being inconsistent, like, you thought you're going to have a list of strings and you put a number in there. That happens, you know, people make those kinds of mistakes, they're pretty inexpensive.
[0] Video: https://www.youtube.com/watch?v=2V1FtfBDsLU
[1] Slides and transcript: https://github.com/matthiasn/talk-transcripts/blob/master/Hi...
[2] Video https://www.youtube.com/watch?v=YR5WdGrpoug
[3] Slides and transcript https://github.com/matthiasn/talk-transcripts/blob/master/Hi...
The reality is I never get to choose. I prefer strong typing. But the projects I am on that ship sailed long ago 2 developers back who used this project as a resume builder.
- Use notepad for development (don't laugh, I've actually encountered a dev who's choice IDE was notepad).
- Never worked with more than 1 person on a project.
- Never worked on anything but tiny projects.
- Never re-opened a project after having worked on a different project for several weeks.
TS has a very complex type system in exchange for relatively weak guarantees about correctness. That's a tradeoff you can choose to take or leave depending on the problem at hand, and holding that position is not the same as believing types have no value.
Unless you're talking about elm or rescript or something then sure sure. But usually when people say things like this they mean typescript.
But all of that makes plenty of sense because in the early days it was rare to work on a real life production JS project that was pure TS from day one in addition to having TS only libraries.
These days it's everywhere and the work has been done to move away from rampant anys in popular libraries. The tooling is also mature and it's rare to find major JS libraries not already packaged with types or a @types import. Turbo moving away from TS was the rare exception.
Well:
% cat a.ts
console.log('Hello, world!')
% time tsc a.ts
tsc a.ts 2.39s user 0.10s system 232% cpu 1.066 total
More than a second to compile a "hello, world" (or 2.4s in CPU time) is orders of magnitudes slower than any other compiler or interpreter that I know of. It's a ridiculous start-up time.esbuild is not an alternative as it doesn't check types.
What I want is "GET /foo.js" to "just" compile TS "on the fly" in dev; it's a simple "just works" kind of setup, but not possible with TS.
Or for a simple system, just "roll your own":
for f in *.ts; tsc $f >|$f:r.js
watch-files *.js --run tsc
(for f in *.ts; tsc $f) | minify >production.js
Doesn't need to be shell, can be a simple JS script or whatever. You really shouldn't need millions of lines to call a compiler even in simple scenarios.What exists now is an explosion of complexity to deal with all this and I guess these systems are "mature", kind of, but for a lot of systems it's massive overkill (and even for larger systems it's not particularly great IMO). Besides, it's really a bad fix for more fundamental problems.
Personally I wouldn't call TypeScript mature until compile times are roughly within the range of literally ever other compiler that has ever seen wide-spread adoption (and with that I don't mean "parallelize to 32 cores so my threadriper over 9000 can compile things in 0.1s). It doesn't need to be fast: just not a huge outlier.
Explosion of complexity and "bad fix for fundamental problems" sounds a lot like you just have an axe to grind.
Many other popular languages have multiple build systems and tools to choose from as well; it isn't a particularly novel challenge.
"You just have an axe to grind!"
Hmkay.
I just want things to compile, with errors, like literally everything else works. It's really not a huge ask. I don't want complex bespoke setups with multiple compilers and background processes and whatnot. The classic JS/TS response is "here is the happy path with all this tooling, but if you want something outside of that then there is something wrong with you".
I guess the "axe" that I'm "grinding" is that I'm having a lot of difficulty using TS in a way that fits with my sensibilities and preferences. e.g. I don't like errors in my editor and prefer to explicitly call the compiler (for any environment). I suppose I could get all of this to work how I want it to with wrapper scripts or whatnot: but it's complex, time-consuming, and isn't needed for anything else.
Based on previous conversations about this at least one person will say something along the lines of "zomg what kind of crusty old backend unix beard doesn't use VSCode and want errors in their editor, you just need to get with the times as you're stuck in the past!!!1" but again: it works for literally everything else (including other compile-to-JS tools), and is not that "obscure" of a work-flow, IMHO, and we're back to a few paragraphs ago: "move outside the happy path and you're screwed".
`tsc --noEmit a.ts` (possibly flipping the position of the flag, I forget if it is sensitive).
That'll check the types without spending unnecessary time compiling with the slower tooling.
From there, esbuild or swc binaries compile the code as desired. They use the same tsconfig file that tsc does, so no need for any extra complexity. They build so fast that you'll not mind having a separate command, I promise.
You could even combine the two into a simple one-liner if you wanted.
Compared to, say, java where you have to fight over maven or Gradle or ant, plus endless config options in XML or groovy or whatever, typescript really isn't much to complain about.
Hell, trying to set up a clojure full stack project is a nightmare of conflicting opinions over tooling between lein and shadowjs or whatever.
Of the big languages, C# is about the only one with a "one true way", and of the smaller ones, they simply haven't yet developed a big enough base to grow contentious enough to have divergent solutions.
The raw output speeds aren't a very good 'practical real world example' as the OP described it.
If anything in dev ESLint (+ Copilot) is the slow ones that I sometimes noticed, but there is already Rust driven replacements maturing in the pipeline as we speak.
I looked in to this some time ago, because I couldn't believe it was this slow, and tsc just has a huge startup cost. Once it gets going it's alright (I think? Don't quote me) but to get started takes a long time. It's actually already improved because not too long ago it was more like 3 seconds (probably because of the parallelisation, which is kind of cheating IMO).
I don't know about Java as I never really used it, but complex build systems are not unique to TS of course, but what they are in TS is mandatory for any reasonable experience because it works around the fact the compiler is so damn slow. That's a huge difference you can get started without too much effort, and you can do "smart" things fairly easily as I mentioned in my earlier comment.
> they simply haven't yet developed a big enough base to grow contentious enough to have divergent solutions.
C, C++, Go, Rust, Python, Ruby, PHP don't have a "big enough base"? Ehhh
And my entire point is that TS LACKS "divergent solutions". Because it's so slow lots of solutions are simply not practical.
Sure it can be an adventure to express something fully in it - but this is true for any type system I've seen.
And I haven't seen another type system that's integrated into a dynamic ecosystem as well (IMO python is 5 years behind in terms of type checking experience) and that lets you chose the level of type sophistication that makes sense.
Static typing, code analysis, automated testing, etc. are all great tools that become counterproductive past a certain point. Where that point is highly depends on what you're doing and typescript is one of the most flexible type systems at letting you make that choice. I'd say a lot of people are terrible at recognizing when they went too far with it for no practical gain.
My only problem with it is that they can't fix the shit JS semantics.
I'm just pointing out that having an opinion about typescript is not the same thing as having an opinion about types. Something I think people are (intentionally?) conflating in these comments.
Too many web developers, basically, I don’t think the conflation is intentional
I still have hopes that Kotlinjs can fix the distribution size and the tooling around binding to js libraries.
IMHO these are markedly different scenarios: the first is essentially just a difference of opinion, the second is a rather myopic attitude.
This applies to much more than static typing as well. Anything built, can be built well or it can be built quickly.
Although I often have to make trade-offs for immediate gain at work, there is a big difference between the quality of my work over time vs the quality of other devs who default to immediate returns. I will say in their defense, they play a role in the team. But I wouldn't want to work on a team where avoiding upfront costs was the expectation rather than the exception.
"I've worked with my fair share of vanilla JS devs who refuse to acknowledge the benefit of testing. Most of them bemoan the upfront cost of testing everything without realizing the benefits of it. I absolutely think less of them as developers."
The really issue with compile-to-Javascript languages is debugging. Have fun stepping through your incomprehensible generated code in dev tools.
(Except Dart which has really good Dev tools support; I guess they can poke the right people to make it work.)
So, how often is poorly tested code out there? From the popularity of strong static typing, it must be ubiquitous.
Static verification (what includes static types) is the real deal.
That said, WTF is there with people insisting that types must be either static or dynamic, and that dynamic types are useless?
Whoever said you can only pick one of the two approaches?
If a developer can jump into an unknown part of a codebase and quickly see that following a certain structure will automatically make their code work for them without needing to read all the code first and double checking if it's just a random convention versus a strict interface so they don't reinvent the wheel or build code that doesn't fit in with the existing structure, then that's worth a lot and something you cannot simply cover with tests.
If anyone can think of something a unit test could test for that an arbitrarily* complex/strong type system couldn't I'd be interested to hear it. It's possible, I just can't think of any.
An arbitrarily complex type system, with dependent types, could detect any bug, since it's formally equivalent to requiring the program be proved correct. But using such a type system is so onerous that I don't know anyone who realistically does it in production.
If you wrote such types, you're basically giving a formal specification of what the program is supposed to do. That could then be used, and likely much more easily, for high volume property-based testing.
More info here: https://en.wikipedia.org/wiki/Curry%E2%80%93Howard_correspon...
if you have a type system expressive enough to do things like that, you've basically got another turing-complete layer on top of your existing language, which is itself ... dynamically typed.
So, how often is poorly typed code out there? From the popularity of test coverage tools, it must be ubiquitous.
In a situation where extreme levels of testing occurs, the extra assurance from strong typing is minimal. In a situation where strong typing is enforced, extra testing is still very useful.
Consider a finite state machine with N states. There are a priori NxN possible transitions. In many cases however most of them are impossible, and you really have only O(N) feasible transitions.
Sure if you make O(N^2) tests you don't need typing... but typing could have made many of those transitions literally impossible. You don't need to test the impossible. For a sufficiently large codebase, you want to limit as much as possible the amount of tests you need.
Also I have more than once identified issues in our testing framework because when I made stronger types I uncovered bugs that escaped our tests. Non-deterministic concurrency bugs are extremely hard to test for. So yes, testing can uncover type bugs, but typing can uncover untestable bugs.
Ok, but it's cheaper to not have to write those tests than it is to write those tests.
That's quite clearly not what I'm saying.
It is like the things you already trust when you code. Let's say, that the file system works, that the network stack of your OS works, or that the cpu works. You can rely on them to use your time for the things that matter. This is a much better model than simply testing everything yourself since, as the tests number increase, any change to the codebase involves more and more test changes.
> Strong static typing is useful in the environment where the code is not adequately tested. That's because tests adequate to make the code bulletproof will also detect the problems strong static typing could detect, rendering SST superfluous.
Your logic is the same as follows: seat belts are useful in an environment where drivers are not driving adequately. That's because adequate drivers will drive in a way to avoid any danger that the seat belt would prevent, rendering seat belts superfluous.
Ultimately, the problem is adequate drivers (as in described above) or bulletproof tests do not exist, they are just a concept. A test can prove the presence of an error, not the lack of errors, and the argument only works in absolutes.
The analogy with driving doesn't make much sense. After all, accidents sometimes happen without the driver being to blame, and the marginal cost of putting on a seat belt is very low.
Apart from an analogy is a template of your argument. It is the logic of the argument itself.
Errors also happen sometimes without type systems being to blame (they only catch a subset of errors).
The marginal costs of a type system is usually very low too, so it seems to be a great fit.
When you use a typed language, you can use your integration tests to actually verify business logic, exception handling, etc. instead of having to write a dozen test cases to make sure that doThing(table, index,*kwargs) doesn't blow up when 'table' is a list or 'index' is bytes...
(It won't find latent bugs that can't currently be exercised, so the testing can't be one and done.)
if you use a static type system you can guarantee there will be no type errors at runtime. why on earth wouldn't you choose that? you can still write logic tests!
and when you leverage the type system to make illegal states unrepresentable, you can make certain classes of logic error impossible as well. some of your tests become tautologies and you can delete them.
What testing cannot find are latent errors not exercised by the program. Do I care about these, though? Arguably these would be found by testing internal interfaces and elimination of code not reachable by tests.
The general argument I am trying to make is that the marginal value obtained by strong static typing declines as testing increases, and that in the limit goes to zero. If a program is adequately tested, is it still worth doing? This is not clear to me, and the arguments given here have not convincingly demonstrated that it is worth it.
Also: if you find yourself in a situation where strong static typing seems useful, you should be alarmed. It means you aren't testing your code very thoroughly.
I am not assuming that.
honest question: what language do you have in mind when you think "static types"? your perspective is so starkly different to mine, you seem to be operating under totally different assumptions about what types can and can't do.
I mean this sentence:
>Also: if you find yourself in a situation where strong static typing seems useful, you should be alarmed. It means you aren't testing your code very thoroughly.
is just baffling to me. it's so self-evidently absurd that I can't even argue against it. what is there even to say?
have you even used a modern statically typed language, with type inference, generics, null safety, algebraic data types, pattern matching, etc? I cannot imagine trying to maintain a big codebase without them. they don't slow me down, they speed me up. they aren't just about catching trivial int-instead-of-string bugs, they are are core tool for modelling data and business logic. they let me define problems out of existence (see e.g. this series of posts https://fsharpforfunandprofit.com/posts/designing-with-types... for an introduction).
and then there's rust and newer-generation languages with borrow checkers and affine/linear types, ruling out entire classes of memory and concurrency bugs ... your test suite cannot rule out data races, but rustc sure can.
you are making the same arguments people were making in the 2000s when most static languages sucked because they didn't have these features. I don't want to go back to a time before sum types.
>What testing cannot find are latent errors not exercised by the program. Do I care about these, though?
you should, because in production your program must endure orders of magnitude more variety in inputs, uptime, and runtime conditions than the test suite can exercise. it can and will get into weird states you didn't anticipate. yes, you can fuzz, yes you can property test, I know all about that. those things are good. but I don't get why you wouldn't also use a static type system to provably rule out classes of problem across all possible code paths. why settle for less?
Because type errors in unit tested code are basically non existent. It's a fictional problem and as such there is no point in spending real resources chasing fictional problems.
There is plenty of research that shows that static typing gives no benefits (statistically insignificant) when it comes to software correctness.
What you are asking is why shouldn't the local government spend money on a Yeti patrol to protect the general public against Yeti's? The Yeti patrol will eliminate an entire class of problem (Yeti attacks).
this is short-sighted. I am not talking about trivial string-instead-of-an-integer errors here. powerful type systems let you encode far more sophisticated constraints on the program. safe rust makes data races into a compile-time error via its type system, for example. unit tests can't do that.
I think you're greatly underestimating what types can do for you. you seem to have this mental model where you would write the exact same kind code with a type checker as without one, and the only difference is whether you have to convince some pedantic bureuacrat that your code is correct when you already know it is.
but when you have a powerful type system, you don't write the same kind of code. the type systems helps drive design, similar to how tests can drive design. you have probably seen code that is bad because it wasn't written with testing in mind. there wouldn't be much value in adding unit tests to the code right away -- you probably need to do some highly invasive re-architecting to make it testable.
so, is it really such a stretch of the imagination that code can also be deficient because it's untypeable? perhaps the reason you don't see much benefit to types is because you didn't write the code with types in mind, as a design tool.
there is a learning curve to writing testable code. the same is true for types.
read this if you haven't, about type-driven design: https://fsharpforfunandprofit.com/series/designing-with-type...
No, my mental model is removing the type checker allows me to write shorter, more concise and higher quality code. Dynamic typing enables much better code styles than static typing.
> the type systems helps drive design
Ahh you are an complexity merchant, if only I made my code more complicated all my problems would be solved. I'm afraid not, the more complicated your code the worse it is.
No I'm afraid, I have actual real commercial experience of doing both styles of software development, the dynamically typed code is the better approach by far.
It's not that I don't understand you, it's just what you are saying is a load of rubbish. :p
> We should not categorically denounce people who prefer it as lesser developers
I don't think you've justified this point. I'm comfortable with my position on this.
It seems reasonable to argue that statements that you won’t even discuss X any more and would rather judge the person as less competent professionally are unproductive. Not saying I agree, btw. But it does seem like a pretty basic point. One of you is talking about preserving your sanity and the other about output. It’s not necessarily a disagreement even.
That is absolutely NOT what the other poster said. They said they _MAY_ judge them that way.
I still think it's reasonable to argue that's not a "productive" approach, but like I said, that's not my own opinion, I just think it's a reasonable argument.
I don't necessarily think it makes someone a "lesser developer" if they prefer untyped languages, but I DO think they've either had to maintain anything long term or it stayed relatively small.
Whether that makes them lesser or not isn't really for me to say, but I can say I'm definitely on board with the idea that types increase productivity the longer a system is maintained. Unless used poorly, types don't automatically mean you use them well, but they make it a hell of a lot easier to do the right thing.
Some issues are settled enough that there is no need to discus them any further. It is okay to automatically mark people on the wrong side of such issues… let’s say ill-informed.
Static typing, I believe, is close to being one of those issues.
Imagine someone is working in a relatively niche new programming language ecosystem which is dynamically typed, allowing the language to have some richness that modern type systems don't support. I don't have an example because I don't know of any such language in 2023... BUT back in the 70s and 80s this would have been Lisp. Lisp couldn't have been strictly typed back then because, AFAICT, type systems hadn't advanced enough to express the sort of metaprogramming that made Lisp unique and awesome. This was at a time when most popular languages were strictly typed.
I would hope that you would keep your mind open to whatever that maps to in the 2020s.
Now, if someone says "I like JS over TS because types are annoying and slow me down", then yeah, I don't have much patience for that either.
The thing is, I've yet to encounter a single instance of such an argument today. Every single time it ends up being "I like JS over TS because types are annoying and slow me down". It hadn't even occurred to me that laziness and sloppiness weren't the the only reasons to write in dynamically typed language.
I suppose what I'm saying is I'm quite interested in seeing what kind of evolution some dynamically typed language could offer in the future. Although with no signs of its coming, I'm going to stick to TS because it's objectively better for anything but very small projects.
At best it's a form of documentation that enables some simple linting rules and a bit of jump-to-definition magic. Like, that's a benefit (mostly -- I tend to think that type signatures implying more guarantees than they provide is a recipe for inadvertently relying on falsehoods), but it's not as clear-cut of a win as you see in other statics/dynamic tradeoffs.
I don't mean to be unnecessarily contentious, but isn't that the point of undiscovered territory?
We haven't encountered it yet, when we do then as awareness grows that pattern, idiom, theorem or concept will be picked up by mainstream statically typed languages.
It's enough to believe that the properties of (for example) JavaScript might lead to an as yet undiscovered pattern that is not possible in current statically typed languages.
Unlikely, but still possible.
The type system enforces those permissions: writing to an impermissible destination is a type error. The types applicable to an entity are not necessarily knowable at compile time; some of them might change at any time.
I had a job working on such a system. It supported a type system implemented in Haskell with both static and dynamic type disciplines, where values were tagged with base types designed to be checked dynamically in hardware.
Was the programming language dynamically typed? Yes. Was the programming language statically typed? Yes.
You can't honestly believe no one who likes to develop in a dynamically typed language has nothing interesting to say about software development?
But that wouldn’t be a wise use of time in my opinion. And that’s what they’re ultimately driving at: we all have limited time to expend, so do it in things that matter to you. I believe “matters to you” is a bias.
Beliefs inform decisions and other beliefs
I disagree with them on a huge number of fundamental things in software. Most of their fundaments are claims with no evidence. Due to that, I simply do not care about what they have to say most of the time.
It is both useful and allowed to have a certain level of belief that won’t be crossed without heavy effort. The OP didn’t even say they were closed to the conversation completely, but that a random person with no pre built trust isn’t going to get the time of day from them to rehash the same settled argument.
from memory Matz -> ruby, Valim-> elixir, Van Rossum -> python, and 'the creators of Julia' -> julia
Not 'random persons' then
This confused me for a moment, but I think you mean annotating the types of the fields in your `struct`s, right?
An addition to that: any non-constant global variables (if you must have those) should also be type annotated.
Dynamic types are nice for quick & short scripts, and actively detrimental for anything long and complex. Why not enforce that? As soon as it gets so long it doesn't run you know it's time to rewrite in a statically typed language. Instead all existing scripting languages allow unlimited growth in code size, making it easy for programs to grow beyond the point where the language is useful.
A long list of big products and companies are there to disagree with this. I am not saying that dynamic is better or worse. But saying that dynamic languages are only good for quick short scripts is a wrong generalization that has not one exception but many exceptions.
But people regularly say the sky is blue, and it's clear as day that it's not when the earth has turned and your side isn't facing the sun.
Only due to Rayleigh scattering do we perceive it as blue, but that’s not due to the absorption and reflection of different wavelengths we associate with innate color. Note that the color changes depending on the angle of the sun, even to the point that it’s purple and red at a few times during the day.
Our perception of the important colors (sky blue, ocean blue, vegetation green, …) probably evolved along with our physical needs.
Don't venture into tornado country; your doubt may be your downfall.
But he's just being honest. Many developers have been making that judgement for many months or even years.
For the very rare cases where a lack of strong typing is needed, most languages with strong typing offer ways to handle that (i.e. the Object and Dynamic types in C#)
It's like if you met a builder who refused to use a hammer and insisted on bashing nails in with the back of their drill. It's not "personal" to say you'd respect that person less as a builder, regardless of how much you'd enjoy having a drink with them.
I'm currently responsible for a very large system built in raw javascript where function definitions like this one in the article: function birthdayGreeting1(...params)
...are the religion.
It's awful. I hold the people responsible in very little regard.
That's a pretty strong take! I don't think there are many things that I feel similarly about... maybe if someone suggested that they don't need test environments and can just deploy changes to prod without testing or CI/CD and just see what happens, when it'd be my employment on the line, but even that's a pretty contrived and out there example.
> The amount of pre-existing respect for someone I'd need to have before I engage in a good-faith discussion on "are types good" is pretty high.
My problem is that not all type systems and the way you use them are equal.
When working with back end code, I really like .NET or even Java having a type system there for me. I know that people suggest that they have their own shortcomings (type erasure, NPEs, no multiple inheritance, even smaller things like C# enums not supporting methods) and that there are better options out there, but generally you can turn off the part of your brain that'd worry about the language too much and just deal with the domain problem at hand. Something like JetBrains are also excellent, because with the type system suddenly the tool also can reason about the language constructs you're using and give you all sorts of good suggestions and refactoring options.
Whereas with something like TypeScript in combination with React, there are times where you fight the type system instead. That's just the impression that I got working on a few projects for a while, in comparison to React with JS (perhaps the code was also a bit too clever), while with Angular it felt more coherent to me (despite Angular being more complex otherwise and not really my first choice). In the end, I gravitate towards Vue with JS for my personal stuff, but it's not like you can just say no to TypeScript when you need to maintain something long term.
I can't actually remember who said that they ditched TypeScript for similar reasons, but the argument was basically that a non-insignificant part of their codebase was there just to satisfy the type system. TypeScript does what it's supposed to... but it feels like it could be easier.
Not even one of my spicier takes, just one of the few that I simply don't care to engage with further.
> My problem is that not all type systems and the way you use them are equal.
We agree. Some type systems suck so badly that I can see why people would be tempted to believe that all type systems suck.
But that's my point: people say that exact thing about Java and .NET, while I find them usable. Meanwhile TypeScript has cool stuff like union types and other stuff to the point where you can get pretty clever with it (https://codegolf.stackexchange.com/questions/237784/tips-for...), which many would describe as the type system being objectively better, yet it's also more difficult for me to use.
In my mind, a good type system would let you do both basic stuff easily without too much work (to make sure that refactoring doesn't make you shoot yourself in the foot) and also encourage you to write the simplest code that you can get away with, while allowing you to get clever in the select few places where that is actually needed.
Which is funny, because adjacent to that, Java and .NET (web) frameworks can be a masterclass in incidental complexity, even though for me the type systems don't get in the way too much.
Edit: actually, I think I'll migrate a JS project to TS, this time in Vue. Perhaps Vue 3 will be a pleasant experience and if it won't, then I'll have a concrete list of things that caused me to feel this way.
That's pretty much my take. I don't hate anyone for having different ideas about it. But if you're going to use Typescript, don't use 'any'. At all. Unless you really don't know and the next step is figuring out what type you've got.
That's actually not hard, you need to be able to run in dry mode, and run the same in an existing instance (in parallel), then compare the results. If you are happy you can continue with the roll up, disabling the dry mode.
> edit: To clarify, I am on the "types good" side of things
I agree. Python 1 would have been worthless if it didn't have types.
Dynamic typing also makes a ton of optimizations basically impossible and even after monumental efforts languages like Javascript are still quite slow, inconsistent and memory inefficient outside of trivial benchmarks.
I thought this meme was dead already. Of course, you might not be able to squeeze out the same amount of performance compared to a brilliantly written C or Rust program, but for what it is, JavaScript is pretty damn fast already.
DOM manipulation on the other hand, is still a very common bottleneck people come across when writing typical JavaScript code.
It's fast compared to other dynamically typed language implementations but it's still very slow compared to basically all of the popular statically typed languages.
Notice that in the Java, C#, Go, Rust, Swift or Ocaml benchmarks almost all of the underlying data structures and much of the networking stack are built in the respective language. This is not possible with Javascript, Python, Ruby etc. because it would be ludicrously slow and extremely memory inefficient.
true
> it's still very slow compared to basically all of the popular statically typed languages.
Not true.
The main slowdown for javascript (AFAIK) is the checks the optimizer has to put into place to ensure the assumptions it's made about the type are still valid. If, however, those assumptions are valid then javascript ends up emitting pretty much the same assembly that you'd see for and highly optimized statically typed language. In fact, there are some circumstances where it can beat a language like C++ or Rust due to the fact that it has to incorporate runtime information into optimizations.
With C++ or rust, if you add dynamic dispatch, unless you are doing PGO and whole program optimization, you are pretty much sunk with 2 memory lookups on every function call. This is the case where javascript can end up beating C++/Rust.
(All of this is talking about hot code after warmup. During the initial execution javascript will almost certainly always be slower).
Part of the proof of this was asm.js, the precursor to wasm. V8 at the time it was introduced could execute asm.js nearly as fast as what firefox could do with it's optimized asm.js compiler. That is, when you stripe out all the actions that make javascript slow, it very often ends up being just as fast as a compiled language.
What stuff ends up making it slow? Generally speaking, stuff that makes the types unpredictable (adding fields, removing fields, sending in a number and a string and expecting the VM to be able to handle both).
You can see a lot of this writeup around the discussions about why Dart was originally "optionally typed". Basically, the entire selling point to make dart fast was simply to remove the abilities to dynamically change types like you have in javascript. With that, the VM authors at the time were capable of making a VM that's every bit as fast as what Java has.
GCC at least is capable of speculative devirtualization by using local heuristics, without PGO. And of course it is capable of devirtualizing in many cases when the knowledge actual type can be constant-propagated.
Also note that the vast majority of calls are not dynamic in C++ (as opposed to most dynamic languages), so devirtualization is significantly less impactful.
ok...
> In fact, there are some circumstances where it can beat a language like C++ or Rust due to the fact that it has to incorporate runtime information into optimizations.
People used to make the same claim about Java and in every single example I've ever seen the Java/Javascript is extremely optimized, performance isn't consistent across VM versions, and the C++/Rust is extremely naive (usually allocating unnecessarily and not using arenas in hot paths that are allocation heavy).
It never happened.
And, you can look at the code yourself, most of the examples read pretty much exactly the same as their C++ counterparts.
Mind you, this is also a test that looks at execution start to finish and doesn't give warmup time (which will always favor statically compiled languages).
> performance isn't consistent across VM versions
That's true for C++ compilers, so why would you expect performance to remain constant with a JIT compiler?
[1] https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
There's a certain segment of the developer population that I don't think realizes just how fast C and C++ are. Javascript is _relatively_ fast when compared to other dynamic languages, but not when compared to C, C++, FORTRAN, etc.
I write Clojure in my day job and it’s insane how often we have issues where it would have been immediately caught by a static type check.
How have you experienced using Type Clojure, spec, Malli, etc. to determine correctness?
I've only worked on solo projects with Clojure, with most of it fitting into my head. I imagine with teams of size N > 1 things can change quite a bit.
Can you describe your usecase for which JavaScript is slow? There are many languages that are slower than js like python or elixir, but they are doing just fine, that's why I won't agree that js is slow, but sure there are cases for which js just wasn't designed, and any CPU intensive task will be slow, but there are ways to get around it as well.
Please provide evidence for this extreme claim. I can point to benchmarks[1] where JavaScript is competitive with or even better than compiled static-typed languages like Java.
(any argument that those are "trivial benchmarks" is automatically invalid without actual empirical evidence)
[1] https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
Nowadays, the static typing is much nicer, with both higher benefits and lower costs, and it makes the costs/benefits analysis much more likely to come out in favor of static types.
In the late 1990s when I was cutting my teeth, I did a lot of Python, and I was almost 100% dynamic language until ~2015. In hindsight, I might do the same again even if thrust back in time. There just isn't a great static option back then. (I'm not saying 2015 is the year it became practical, I'm saying that's when I finally moved into static languages. 2010-2015 or so I was in Erlang, doing things that most other languages couldn't do at the time, so that forced me into a dynamic language. C# was looking pretty good in that time frame too, it just wouldn't have run the systems I had on anything like the resources I had at the time. It could probably easily do it in 2023 though.)
Now I even prototype in static systems, and it's a better experience than prototyping in Python was. Like, by quite a lot, honestly. In the end, I don't find it that much of an impediment to make sure that if I want to call a method on a thing, that the method actually exists.
Dynamic typing will never disappear; there's a certain small size of task for which it'll always be more advantageous than static typing, and while said tasks may be small, there's a lot more of them than there are large tasks, so it's a completely valid and sizable niche. But I do think over the next 10-20 years we're going to see the "scripting" languages return back to "scripting" and away from "systems".
I think that in the end, the dynamic scripting languages being used for large tasks will be seen as a reaction to a misdiagnosis of the problems in the 1990s. The code was atrocious in the 1990s not because it was statically typed, and therefore the solution is to go dynamically typed. The code was atrocious in the 1990s because it was poorly statically typed, and the solution was to get better. That said, "getting better" did take a long time, and for many legitimate reasons.
(Much ink is spilled on the so-called rapid pace of technological innovation in our industry, but programming languages move on decadal scales. Programming languages still have only barely grappled with a multicore world, and haven't grappled with a heterogenous computing world at all (GPUs on one end, efficiency cores on the other). Things are not always in as much motion as we fancy.)
[1]: Particularly, Hungarian notation is supposed to supplement the type, not just reiterate it. If you're in a language where you can't easily declare "a width is an int", then Hungarian notation suggests calling a width variable something like "wdthDialog", so that you stand a chance of noticing that you passed a "hghtDialog" in the wrong place. But the way it was used a lot of the time is you got "u16Width" instead, where u16 meant unsigned 16 bit int... but that's already in the type. Using it that way just adds an extra layer of hierglyphicness to the already ugly code. One of the several innovations that made static typing languages more feasible is that in most languages designed in the last couple of decades, you can declare something like this with something like "type Width int", and then you don't need to label any variables with it at all, the compiler enforces it.
1. It's simple. Let's not lie, let's not pretend. It's CRUD. You can put in K8S, you can add AI, it can be behind an API Gateway, you can make it all Event Driven, you can use CQRS for every entity because you really, really want to feel clever. But it's still CRUD.
2. Other people are working on it, many other people.
3. People from other companies are interfacing with it.
So knowing what to expect trumps everything. Types help with that. They help a lot.
To be clear: people have tried to show the long-claimed safety benefits for a long time, and they just refuse to appear.
What have you built or added to the field that justifies such a lack of tolerance of those who don’t subscribe to your point of view?
That said, yes, of course my post is arrogant and dismissive. I am outright saying I won't engage in a conversation. I'm comfortable with that.
It's actually arrogant and dismissive because you say that in a way clearly intended to spark a conversation.
Also, pretty much all arguments for explicit typing suffer from mechanistic bias (see, https://www.youtube.com/watch?v=NmJsCaQTXiE&t=143s). I'm comfortable saying I just enjoy tinkering with a formal description of a data structure, because I'm a type enjoyer, not a type fan.
I don't follow at all. Why would it be arrogant or dismissive to say something in an attempt to discuss it? I commented in good faith to an article that has a strong opinion ("willing to die on this hill") with a reflection of my own strong opinion ("unwilling to discuss").
At most you could say it was inflammatory, which wasn't intentional although looking at the insane number of child comments it apparently ways.
Types are integral part of standard programming languages. If you mean "types bad" as in BASIC/Javascript variables, then yes I fully agree. These languages were never meant to be a solution for professional software engineering.
Once a language has proper types, it can be "weak typed" by not forcing "strong typing" by being dynamically typed in nature. If the language has normal preprocessor support, the static type system can be added on the project level. And then in essence you get a strong typed environment in a very thin programming language such as C.
Let's take a big C or C++ software project, the process behind it. Of course, the project is in those languages because it requires native opaque pointers and hardware access. The project has coding style, it has arbitrary rules. Although there is little to stop anyone from making a mess in C or C++ code there is entire code infrastructure and CI/CD chain around it. Lets say the rule is no void pointers without encompassing struct that's a type that can be checked via macro system. If you wrongly use the type struct, you get stopped by the project build. If you go around the rule, you get stopped by a code analysis after commit (and get in trouble for doing that).
I think that says more about the strength of your zeal than of your argument.
if you get the static type system wrong once, there's no going back
Take for example, Rust: mut, Send, Copy, Drop, Debug, dyn have all their warts because they are special and you can't fix them because you'll break previous code.
In a gradual typing language you could potentially bolt-on your own type system as a package where it just runs the type checker if you want one. It just so happens people who favor these gradual typing systems don't do a good job of creating those typing systems because they are not that into types in the first place.
But in theory, this kind of a system would let you not worry about types when prototyping the system, then put some bounds later based on your requirements.
SML or Haskell? Yep, like those ones. The languages would not be so useful without their compile time type checking.
C? Not worth the trouble. C++? Not worth the tarpit or the trouble.
Python? Definitely not keen, that language was much better without the annotations.
Typescript people really like but I haven't played with. I'm pleasantly surprised that unsound static + sound dynamic works well.
Type annotations are not inherently good. Some type systems add a lot of value, some really don't.
lots of comments here praising Haskell and Rust frankly seem to be coming from people with a superficial understanding or limited exposure
I've had rustc complain about type deductions that were over a line long...that's just one inferred type...you can easily paint yourself into a corner with a type system with no exit other than trying to cajole the right definition out of the compiler and then you copy-paste it and pray
The OP said strong static type system, so C and C++ are out of the question.
People who say they prefer dynamic languages are really just saying ‘no’ to all these free benefits.
Otherwise the cost is merely being unable to write anything that the type checker does not understand, and however long your compiler takes to do the checks on what it does understand.
Also a fair chance the whole program must type check before you can see the results of changing a subset. In the worst case you get to hunt down all the unused variables before it'll run the test suite.
I prefer dynamic languages. That makes me a heathen on these boards, to be ignored or chastised for my stupidity. Regardless, those benefits do not come for free.
- having a compile step in between writing and running your code. This drastically slows down the feedback loop of development. The more your compiler has to check for you, the slower it gets.
- being able to run your program in a half-broken state. This may not seem like much of an advantage, but sometimes it is good to just be able to run a broken piece of code to see how it crashes. This is especially important when learning to program, but is still very beneficial when learning a new language or framework, or sometimes for debugging.
- As someone who has contributed to the main Haskell compiler, I can definitely confirm that a more complicated type system can slow down development of the language itself too. It takes significantly more effort to grok all the possible interactions as the codebase of the compiler grows.
Don't get me wrong, I think encoding and enforcing program properties with types is a great idea that will grow further in the future. But the dynamic languages gained popularity for good reasons, and some of those reasons are still valid today.
Not strong enough.
If you are proponent of strong typing, you need something that is effectively a theorem prover. I.e when you define a type, you define the scope of the data it can hold, operations on that data, and the resultant types of those operations. That way, when you code compiles, it is by definition "correct".
When you accept anything less then that, you are basically making a statement that you are willing to forgo some of that correctness for convenience, which is fine, but that means that no language out there is really good or bad.
That said, your position is also just weird to me. Yes, theorem provers have nice type systems that are wonderfully expressive. The trade-off is that some common programming patterns become difficult to use or are even impossible.
When it comes to everyday programming, I don't think theorem provers are at a point where they are particularly useful. Not everything needs to be proved formally, and I don't think this position is at odds with the belief that static type systems are generally "better" than dynamic ones.
Not really. Strong and expressive typing at its core is simply creating data packaging containers that have defined operations on them. It says nothing about logic. You may be referring to the functional programming aspect that comes with strong typed languages, which is related but not the same as strong typing.
The point is that typing is just a tool that a programer can use, whether its built into the language or ran statically like MyPy. You can take a piece of code in C, and write a test suite on input and output of that piece of code, and accomplish much of the same thing that typing accomplishes. However, in the case of strict+explicit typing, the idea is that you wouldn't need tests in the first place, because your code would be correct by nature of compilation.
Some theorem provers, such as Coq, are not Turing-complete. This means you cannot write some programs, and in particular you cannot write infinite loops in Coq. Infinite loops are a common pattern (e.g., a REPL or a GUI display waiting for input).
This is a trade-off, as I said. You gain the expressive type system, but lose the ability to implement certain programming patterns. It has nothing to do with the strength of the type system.
---
> whether its built into the language or ran statically like MyPy.
All (true) type-checking is static, so MyPy "running statically" is not a noteworthy feature to distinguish it from other type checkers. I guess this point may seem trivial to some, but the broader context of this conversation involves conflation of the terms "static type system" and "strong type system", so your misuse of the word "statically" here seems worth pointing out.
That said, whether you use an external tool to check your types or the type-checker is built into the compiler is irrelevant, and I'm not sure why you brought it up at all.
---
> You can take a piece of code in C, and write a test suite on input and output of that piece of code, and accomplish much of the same thing that typing accomplishes.
Depending on the perspective, this is factually incorrect.
Type-checking is an ahead-of-time operation that guarantees the absence of certain classes of errors at run-time. Writing tests to check for the absence of such errors is not equivalent, because you have not proved anything; you merely gain confidence. They are semantically distinct, even if you write many tests to gain a lot of confidence.
You are talking about languages, Im talking about the concept. The modern theorem provers aren't up to the task. Due to Rices theorem, you cannot "fully prove" a program, so a language that does this couldn't even exist. The strict typing however can be applied to subsets of the programming space, namely the data processing pipeline, whereas higher level stuff like the actual server code that does have an infinite loop to listen to requests can be written in whatever.
The point is that no language recommended for strong type safety today is anywhere fully complete to include the rigor of something like a theorem prover, and anything less then that is basically your own opinion on what is "good enough".
>Type-checking is an ahead-of-time operation that guarantees the absence of certain classes of errors at run-time.
Run time errors are no different than compile time errors as far as testing is concerned. Its not like the computer blows up when you have a seg fault. And you absolutely can prove what you need for operation.
Say your input to your code is an HTTP request of length x. And output is some data processing on that request. You can write a test suite that is basically like this
1. Ensure that code returns a well defined error for x values outside of given range.
2. For all valid ranges in x, test all possible values of every byte in that range, and ensure correct behaviour.
While overkill, this will absolutely exercise every single piece of your code and prove correctness. You can also couple this with checking things like memory access
if I store sql with a column definition that stipulates that the value must be an int between 1 and 3...what difference does it make if I accidentally create a corresponding variable with the value "fred"? it will never be persisted
furthermore, you can run in to even more confusion with a language type that is not properly aligned with the database type...which one wins? the db obviously, since language values not aligned with database definitions will never be persisted
I'm not going to argue that these features are only possible because of dynamic typing, but regardless, statically typed languages tend not to have them.
Elixir may get "some form" of typing, but it likely won't be traditional static typing.
For Elixir, the concurrency model and supervisor trees. It's a perfect fit for some problems, and in general as a small company, the projects we use Elixir on greatly simplifies our production environment which is always a win.
Most of my focus when designing a project is to enable people with less experience than me to contribute in a bug-free fashion in the face of concurrency and parallelism. Sometimes that involves picking a funny language, sometimes it doesn't.
I've been writing code for 40 years, I can't estimate how many loc I've written, but likely over a million. One of my personal projects is currently over 65k loc vanilla js, and I never once had a problem with not knowing what type a function took. If you're so bad at naming things and knowing what a function does, I guess maybe types can help you. But not everyone needs it.
I think such a debate is largely unproductive. Like anything else, types have value (ha) and successfully capitalizing on that value depends on the context of the project, which includes things like developer experience, tooling, project complexity, requirements, deadlines etc.
The only productive outcome of these debates is that each developer gets to slowly and frustratingly build a list of pros and cons as they go through the arguments presented by either side debating this topic. In addition, developers who completely disregard either the cons or the pros are necessarily making subjective decisions with incomplete data, and the project gets to pay the price. Just because the developer is personally OK with all of their projects paying that price, doesn't mean it's the best decision for a project.
My experience has been that when starting out, projects get the most value out of not having types, and as they grow in scope and size, and the cons of not having types start creeping up, that's the point when gradually transitioning the code to being strongly typed allows the project to maintain its velocity _and_ quality.
Many years ago I thought that it's a good idea for a game math library to have separate strong types for 'point' (a location in 3D space) and 'vector' (a direction and magnitude in 3D space), and allow/disallow certain operations (e.g. 'point + vector => point' 'vector + vector => vector', 'point - point => vector', while 'point + point' is illegal).
Sounds absolutely great in theory, but in practice it was a royal PITA to work with, but it took me much too long to realize this (how can it be such a hassle when in theory it's such a good idea!)
I soon went back to a general 4D vector class (where a 'point' is defined by .w = 1.0, and a 'vector' by .w = 0.0), and some debug-mode runtime validation (which catches things like trying to add a point to a point).
Of course strong typing also often makes perfect sense, for instance in a 3D rendering API it should be a compilation error to provide a texture-handle where a buffer-handle is expected, but after this experience with points vs vectors (which should've been a classic showcase for strong typing) I would never again "die on that hill" :)
TL;DR: static typing: yes! strong typing: it depends.
Those help you more if you do a lot of math involving both them, but that’s also when it becomes a nightmare in most programming languages because of an explosion in the number of types.
Let’s say your code computes
3km × 4hours
If so, you need a “km hour” type.In many languages, you also have to write code to make that happen, and to make it have the same type as
4hours × 3km
Even if you don’t ever store values with those types in variables, you also may need types for per km, per hour, km² and hour² for expressing the types of intermediate values (for example, in a physics computation, you may encounter √(3km²/4hour² to compute a velocity in km/hour)And that’s ignoring that you may
- encounter minutes, meters, yards, etc.
- want to use algorithms that compute exp(3km) or log(4hours). What types do these have? Here, you probably want to forget about string typing values.
The good thing about stricter typing systems is that it forces you to handle all the cases when you're doing a refactor like this before it compiles.
When changes to the data model happen in large codebases of dynamic code, it frequently gets shipped in a non complete state and turns into a production runtime error later down the line.
cries at the thought of insanitybit not respecting me
The feeling is mutual.
discussions are a two-way street. if you aren't getting it, then how do you expect someone else to "get" your position?
"The whole problem with the world is that fools and fanatics are always so certain of themselves, and wiser people so full of doubts"
-- Bertrand Russell[edit: To clarify, i am on the "i like strong static typing most of the time but also understand their pros/cons wrt. dynamic languages" side of things]
I probably love assembly language more than C#, or C++, or any of the strongly typed languages I use.
Assembly language has no types. It doesn't pretend types even exist.
Are you going to look down on me because I like to write assembly language?
Nope.
Say you can do the project in 2 ways.
First way is to use a strongly typed language, think about the data, and design your code in the appropriate way. You code it up, go through the loop of compiling and fixing errors, and get your code to run.
The second way is to write your code in Python, without worrying about strong types. You complete the code quite a bit faster, but since you also want your code to be correct, you spend time writing an end to end test suit for your code.
The second approach is not only faster (since you are writing tests in both cases), its overall better. Spending time writing tests allows you to essentially validate things that modern mainstream strongly typed languages can't catch at compile time (for example, what happens when the input is unicode strings?). It also forces you to think about end to end behavior and making sure that is correct, rather than just the behavior within your code.
Strong typing is a simply hand-holding tool for programmers. If you cannot write correct code without it, you are on a fast track to being replaced by AI eventually. The future of programming is not going to be designing data structures and types, its going to be using English to generate large chunks of code in most likely Python, and then tweak fine details in those.
I honestly don’t know if we’ll still program by hand or not, but I do look skeptical at people who are very certain we won’t. Don’t think that’s the first time in history people make that prediction…
As far as adoption, there is a reason why dynamic type languages that feature a lot more natural syntax (Node, Python) are used WAY more than others.
also it's pretty ridiculous to say "static type systems are handholding" and then say the remedy is to write your code with AI ..
This is, in fact, MUCH easier to do with a strong test suite that only cares about input and output rather than internals.
The most common approach to starting a refactoring project is write or enhance a test suite to the point where there is no undefined behavior, either the program works or handles the appropriate errors.
You don't need to aim for 100% test coverage either, you just need your tests to cover all possible inputs (including fuzzing).
There's only explicit typing and implicit typing.
So the only real argument you're having with someone is when writing a piece of code, do they want the caller of the code or the input of the code's data type to be known or they want the data type to be a mystery to be figured out, occasionally in production when shit hits the fan.
> That said, yes, of course my post is arrogant and dismissive. I am outright saying I won't engage in a conversation. I'm comfortable with that.
This is an extremely toxic attitude. I wouldn't be interested in working with you in any professional capacity, on an open-source project, or having you as a friend, and as a matter of fact if someone expressed this attitude at work I would complain to HR.
I now program exclusively in python. I am never going back.
I have tried and do have some of the
def hello(world: str) -> str:
But to date I have not found a single example where this was useful at all. In fact, I found some odd cases where it's harmful.
No typing for me thanks.
As a falloff expect decades of engineers lamenting overly engineered, overly verbose, overly complex TS legacy apps.
If anything, the dynamic typing bubble is bursting right now.
But then again, I suppose we haven't met.
It is because the enterprise TS supporters are too loud.
There are many here saying they consider people that dislike TS to be idiots and they don't lower themselves to discuss with those who think JS is preferable.
They don't wanna argue, we don't wanna argue. Life is great.
I don't think we're near the peak yet. We're still on the way up. I've seen $5M projects turn into $50M projects in the space of 5 years and nobody else in my little consulting bubble has noticed at all lol. It shouldn't take 100 devs to build a SaaS with 5 forms and a dashboard but here we are...
Static typing allows me to check the types are correct! In practice, typing related errors are a very rare bug so it isn't very useful.
Static typing allows me to check things at compile type. In practice, this means waiting around for things to compile and lots of dead time. The larger the project get the longer the compiles times.
If performance doesn't matter, dynamic typing is pretty much always the way to go. You get the code out faster, so you have time to write more tests and you can throw away the code faster. Cause in the real world, code is a churning thing that evoles with the business requirements. Not some static build once thing that static typing and Junior Software Developers assume.
I'm having trouble taking seriously a post that beats this dead horse.
Sure, it's there. It's even documented in the language specification, but now that we have template literals it's rarely used because truly, what is the expectation when using addition on two variables of different types?
As for static typing:
Grug put it eloquently: https://grugbrain.dev/
> grug very like type systems make programming easier. for grug, type systems most value when grug hit dot on keyboard and list of things grug can do pop up magic. this 90% of value of type system or more to grug
> big brain type system shaman often say type correctness main point type system, but grug note some big brain type system shaman not often ship code. grug suppose code never shipped is correct, in some sense, but not really what grug mean when say correct
In my opinion static typing is useful and I wouldn't start a commercial project without it. That being said I think it's productive to practice programming in dynamically typed languages, because:
-You avoid the sort of type golf that happens with a sufficiently powerful type system(I regret every use of the `infer` keyword in TypeScript).
-The situation forces you to be radically explicit.
-You can explore situations that would normally be hard/impossible to type.
Static typing is a good choice when the system you are implementing is already defined. It's great for implementing a well-defined algorithm or protocol, or a well-studied domain like game engines or financial exchanges or rocket ships. Basically, wherever correctness is necessary and possible.
Static typing is a bad choice when the system you are implementing is largely undefined, or actively evolving. That is, "the rest" of the software, where "software correctness" is undefined. Which includes things like a business, a new video game, a website, and scientific research. Because you will waste time building an ontology of types that can't possibly be known at the time, or worse, constrain the natural evolution of the project.
CS students often get a warped perspective of software, where everything is well-defined by their professor or some textbook or RFC or some other smart bloke, and they just have to implement it. In reality, the average software project is not this way.
The end result is Enterprise Java (formerly C macro hell). You get a bunch of CS grads who think static typing is the only answer working on some fuzzy business logic with Java. The conclusion is an unspeakable monstrosity of bad abstractions (and job security).