In your example, can you explain what exactly is the issue? Is it that foo is used as an object in the condition? Instead of being called as a function?
How Typescript catches this as an error? What if the condition actually wants to check that the function is defined?
Is the code really broken if the program runs without defects?
It’s hard to find arguments against guard rails in general. Can you really argue against more stop signs? More pedestrian crosswalks? More protection gear when cycling? Argue against more padding around sharp edges?
The one good argument against in my opinion is the one Paul Graham makes here: Beating the Averages
At my work we’re one of the only Clojure teams, and our team has a reputation for being one of the most reliable at delivering on business impact and value. Stakeholders like working with us, because we get things done. Other teams are using Java or Scala or Kotlin. We own twice as many services as the average team as well, without needing double the engineers.
We also have some of the least defect rates, we work on live services, and have great availability, uptime and the number of support issues and incidents that are due to an actual code bug are minimal, less then a handful per year. All that gives us a pretty good reputation internally of exceeding expectations.
Now, I wouldn’t switch to JavaScript from Clojure and expect the same. That’s why I think all conversation about type checker on their own is irrelevant if you don’t consider the rest of the language as a whole, because the end result is the whole package.
Anyone who comes from JavaScript and proclaims static typing is better has a skewed bias, maybe it is better for JavaScript given everything else about JavaScript. That might not mean it would be better with Clojure, especially if it means sacrificing other things and thus restricting other features or properties of Clojure in the process.
I insist here, because this is a very common fallacy in those discussions. Clojure from the studies I’ve seen has a much lower defect rate than Java, but Java has a much lower defect rate than JavaScript. Yet I’ll see people generalize to say that static types prevent defects, except Clojure has less defects than many statically typed languages, what gives?
My team also maintains a few Java and Scala services, and they have measurably more support issues and incidents that actually are due to a code bug, they also often have more functional bugs, as in they failed to really meet the spec, even if the code doesn’t have bug, these are insidious bugs, but Clojure I think catches those because the REPL means you’re more in touch with the actual runtime behavior, and you more quickly realize wait is this really the behavior that makes sense for the use case?
Again, I wouldn’t be surprised if we switched to JavaScript that it would have even more defects and bugs of all kinds.
That’s why personally talking in the general sense of static type checkers or not doesn’t seem as useful as discussing the actual languages.
If you ask if Clojure is a safe language that leads to low defect programs, or if it is prone to accidental bugs or not, that’s a much more interesting discussion. For that, we also more easily find no evidence from the research analysis and from annectdotes in the wild, or from my own experience. It actually seems like Clojure is on the safer side of the scale, and tends to produce quite low defect programs.
Ask the same for JavaScript and you might have a very different answer.
That’s a misunderstanding of types I believe.
The casual language has gotten a bit confusing so it’s no wonder people misunderstand types.
A static type, is when a variable or container for values cannot change the type of values it contains at runtime.
A dynamic type is when a variable or container for values can change the type of values it contains at runtime.
In both cases types exist.
An untyped variable is one where the type of values a variable or container contains and can contain is unknown, and really it’s just a pointer to a memory location, with no knowledge of what the thing it points too is at all.
Now it turns out that if you wanted to build a validator which is often called a type checker, that could run at compile time, and infer from the source code itself that there will not be assignments of the wrong value type to the wrong variable type at any given point in time of running the code, that if you allow for variables to contain values of different types at different times, it becomes an impossible or not currently known how to implement such a type checker that could truly reason at that level of dynamism.
Thus to build a type checker that can reason about runtime type correctness from the source code only, what is casually called a “static type checker”, you must also enforce a runtime that will not allow for dynamic types, but will force all variables or value containers to have static types, that means that the type of values they can contain cannot change. Well or enforce that your compiler doesn’t allow to compile those programs.
On top of that, there’s other challenges for building such a source based type checker, like allowing a variable of value container to contain a variant of possible types makes things a lot harder as well, so supporting heterogeneous variables or value containers tends to be trickier and if you really want such type checker you might also have to enforce a runtime that doesn’t allow that, such as forcing a List to only contain homogeneous types.
The distinction I’m making is that on one hand you have a program that can reason about type correctness given only source code and some constraints/properties for it to prove will hold of the running program. This doesn’t have anything inherently related to static or dynamic types, except that it’s impossible or very very hard to implement one for the case of a program that would allow dynamic types. And due to the complexity or impossibility of it, you accidentally have to restrict the runtime types to be static.
This made it so much so that we even started calling languages with such a type checker statically typed languages, and those without dynamically typed. In theory, it’s not necessarily the case, you could have a statically typed language that doesn’t have a source based type checker, but nobody does because why would you impose such a restriction without any other reason? The trade off only makes sense if you gain something else in doing so.
Now that I’ve made this important distinction clear. If you look at Spec, you realize that Spec doesn’t actually make types static, in fact Spec is able to validate dynamic types, because it too is dynamic in its validation.
Thus in no way is Spec a form of static typing or forcing static typing on the program.
Type hints on the other hand do enforce static types, allowing the memory to either use primitive types which are more compact and efficient, or allowing the compiler to hard-code the dispatch directly to the method of that exact type without needing dynamic type inspection for the dispatch, which again is more performant.
That use of static types is clearly better, if you care about the performance and memory efficiency, I’ve never seen someone claim otherwise to be honest. Some people can claim it’s annoying to force it, if you don’t mind the performance impact, why force it? But having it as an option is an all around positive in my opinion, I see no trade offs.
This is another slight misunderstanding of nuance that I sometimes get pedantic over haha. Spec is not like gradual typing, Spec in fact doesn’t even deal with types at all. Spec deals with values directly, and uses predicates as constraints. If a type checker could reason about source code from those predicates it would be awesome, but it’s kind of a very different approach.
Where my mental model is still fuzzy about is around Dependent types. Do they allow reasoning about dynamic types from the source code? Do they allow a correspondance between predicates and types?
Gradual typing is an interesting approach, let the programmer decide which trade off to make when and where between the two.
I think the downside has been that it reduces quite a bit what the type checker can prove and so in practice you might find that the added effort of annotating types in the source isn’t worth it if it doesn’t even let the type checker guarantee that it works in the entire code base. Especially because we use so much library code nowadays, of they’re not annotated, all code that depend on that unannotated code can’t be reasoned about and that’s like 90% of your application.
Edit:
Ok, this is already very long, but I’d like to also bring up the consequences to Clojure of all this.
You know the pattern of having a single Atom with a heterogeneous Map to represent all of your application state? That’s very dynamic in type. The values this Atom is allowed to contain can change over time, the values this map can contain can change over time, it can also contain values of different types.
The entire data-orientedness seems hard to reason about from the source. There’s quite a few patterns of structuring values and code I think that wouldn’t allow for a type checker to reason about we’d need to get rid of.
I think that’s the struggle a type checker has if we want one for Clojure, how do you retain the current look and feel of Clojure, keeps it’s current idioms and patterns, the same.ergonomocs, but also allow a type checker to reason about the types of it at runtime, without just forcing Classes back into the language, and going back to homogenous collections?
Even Haskell feels more OO in a lot of ways, because it defines all these “types” that’s like half the ergonomics of an OO language is having to write a definition of a static structure to contain values of types that can’t change.
Point {
x : int
y : int
}
That’s technically not OOP, but it sure start to feel like it.
Now you want to put a long in there? Sorry, why don’t you define a PointLong type?
PointLong {
x : long
y : long
}
Well that’s annoying? Okay, why not spend an extra 12 months of man effort to add generics?
Point<T> {
x : T
y : T
}
Wait, you want a Point where x is an int and y is a long? Err… damn you… Maybe add a runtime cast?
Ok fine, let’s spend another 12 month man effort to add union types:
Point {
x : int | long
y : int | long
}
What is that? You’re worried this allows invalid points in places it wants a Point of x and y of int, and that no where in your program are Points of x long and y int allowed? I’m sorry, I’m out if ideas.
Is this really the feel you want for Clojure? I’m personally not sure if it would bring me the same joy if it was the case.