OK.
> Thanks for the segway.
A segway is a motorized transport; a segue is a transition to a different topic. So this could be considered a type error, and demonstrates why you need types, not just immutability.
Complex static type syntax ends up infecting your code, because the syntax becomes load-bearing. A change to one type has a tendency to ripple through the entire body of code. This means you have tight coupling as a consequence of the syntax, which is a bad trade-off. Clojure's strong dynamic abstract types allow you to focus on the function of the code instead of its structure. Trying things out in the REPL first helps you find many of the things that you might need to wait for a compiler to find.
There are studies that estimate that the kind of bugs shipped to production that strong static types help prevent account for only 2% of defects. The rest are things like off-by-one errors, design errors, and incorrect logic.
The other thing a static type system can help you with is reading code. But it's primarily an aid to people unfamiliar with the language, or put in more popular parlance a "skills issue." We know tooling can navigate the code, because we have tooling that does so (language servers, CIDER, Calva, Cursive, Conjure...). This means that static typing for the purpose of merely reading the code is a crutch. I'd argue it's also a crutch or training wheels for writing correct code.
You can't run with crutches. You can't corner well with training wheels. Clojure is meant to make the expert nimble and fast. The main thrust of its design is to make experienced devs more productive, like professional tools without novice guide-rails make experienced craftsman more productive.
Rich Hickey gave a talk where he mentioned this idea of guardrails on the highway. He told a story of how we all get in our car, then bump into guardrails all the way to our destination... right? Of course not, we learn to keep the car in our lane of travel, to signal others when we switch, and to blend with other traffic on the way. (Rich was talking about TDD, but it still applies to static type systems, IMHO.) Clojure is that power tool without all the novice presets. It's the automobile we keep away from the rails when we drive it.
Mutable state is arguably responsible for far more defects shipped than type errors. Mutability is only a thing because hardware constraints required reuse of memory locations instead of allowing for persistent data structures. Thankfully, for "situated" applications that start and operate for extended periods, those constraints aren't so tight.
If you have to choose between immutable data and static types choose immutable data. But why not both? We can have all our luxuries, right? Because you have to spend so much more time proving things to the compiler, you have so much more syntax to manage. More code and more syntax for the same problem is almost always worse. I'll take Clojure.
If you've got a reference, I'd be interested. (Not saying you're wrong...)
> The other thing a static type system can help you with is reading code. But it's primarily an aid to people unfamiliar with the language, or put in more popular parlance a "skills issue."
It's an aid to people who are unfamiliar with the program, who have no idea what the shape of "that thing" is. (Especially, what is the shape of the thing that this function gets called with? For all possible callers?)
I've been hired to start working on code bases that have existed for a decade or two. Static, stated types are a life saver. Sure, they may slow you down while you're creating the code. It's easier to not have to explain everything that's in your brain. But when you and your brain leave, and I'm left with a bunch of variables that I have no idea what type they are - what are the maximum set of types that they can be for all paths through all callers - then I really need static types to help me figure out what's going on.
"You Keep Using That Word, I Do Not Think It Means What You Think It Means"...
When you say "types," can you please give a concrete example of what languages you're talking about? Whenever someone talks about Clojure from type theory talking points, they make it sound like it's completely untyped or weakly typed. Clojure has its own type systems, and you can express things in them that are far more difficult to do in some other languages, even with static types. It is also a strongly typed language, and in practice, I feel far more confident about our Clojure code than anything written in TS or Java.
Sure, static type systems have their value, and some of them are really nice. In practice though, whenever I have to jump in to deal with Java, Typescript, or even Rust code - there's so much seemingly unnecessary fluff, and I don't feel the practical worth of dealing with types - it almost always feels taxing. Haskell/OCaml is a different story - I can buy that one. But realistically, getting to the point of writing practical software in Haskell is an incredibly bumpy road, and (relatively) few programmers successfully achieve good milestones on it.
But I can't expect most codebases to use spec or malli. The comment I replied to said that's what the REPL is for. A REPL, while helpful for a lot of things, will not tell you about an argument faster than a type annotation.
[1]: A Large-scale Study of Programming Languages and Code Quality in GitHub https://dl.acm.org/doi/10.1145/3126905
[2]: On the Impact of Programming Languages on Code Quality: A Reproduction Study https://dl.acm.org/doi/fullHtml/10.1145/3340571
Clojure sort of guides you to simplicity, building everything out of functions and simple datastructures has big advantages when testing and reasoning about code.
I do find that in larger code bases, Clojure lack of types causes friction (spec is just a bandaid, not a fix).
There are languages with immutability and types (like Haskell), but these don't have the get-shit-done factor I seek.
Interestingly, most of the low level libraries in the Clojure ecosystem are programmed that way. If you open up the code of many popular Clojure libraries, you see defprotocol everywhere.
So really, Clojure does both: dynamic typing for application programming, mostly static typing for infrastructure library code. As they should be.
This tired argument of "Clojure bad for dynamic typing" just doesn't hold water. Keep repeating it is a sign of lack of critical thinking.
There is Typed Clojure https://typedclojure.org/ for people who want actual static typing, but the fact that it never caught on suggests this wasn't a real pain point for most people using Clojure.
As other people mentioned, immutability tend to be a more important feature than static types because it makes it easy to write code that's referentially transparent. You can structure your whole application as a series of small components that can be reasoned about in isolation.
If you write code in a defprotocol everywhere style, as many Clojure libraries do, your code won't compile if you got the types wrong. The same as Java. How's that not static typing? Which part of that is weaker?
So what exactly this "Clojure dynamic typing" nonsense is about, I fail to see.
Automatically inferred type system is not the same thing as static typing. Typed Clojure is the former. Typed Clojure did not catch on, but static typing style of Clojure did, as many Clojure libraries do exactly that: internally, defprotocal everywhere, externally, some Clojure functions to give the illusion of normal Clojure code. BTW, that's the style how Clojure itself is written in as well.
No-one has the time to learn all these languages, so for those of us not in the know, the most generous we can be is to take these praise articles at face value. And these articles are typically 'dynamic good', 'static bad'.
From the article:
they insist on a statically typed worldview leading to parochial, snowflake APIs that defy abstraction and higher level manipulation, or both.
So I guess the user base for this language is those developers who think dynamic is better than static, and go for (as-static-as-Java) Closure.I find using defprotocol in Clojure tends to be an antipattern because it just makes code harder to read by introducing indirection. The libraries using defprotocol use it to solve a specific problem of creating a contract for how the API looks.
It is not an anti pattern, it is the way most low level libraries and clojure itself are written.
Clojure is a tool, not a cult. This core team worship is turning people away. The core team made plenty of mistakes, and got called out, rightfully.
Most libraries are absolutely not written in this way either. Please point me to a single library that's actually written in the style you describe. The use of protocols in actual popular libraries like Ring tends to be minimal.
The reality is that dynamic typing has never been a real problem in Clojure. I've worked with the language almost exclusively for over a decade now, and I maintain a number of popular libraries, like Selmer, with millions of users.
Clojure gives you the option to run the compiler at runtime, so that's what people normally do. However, you can also run the compiler at compile time. Right?
For people who want type checking, they can opt to write Clojure in this defprotocol everywhere style, and turn on AOT. Then they basically get the same thing as what Java gives them.
As to example of libraries that are defprotocols everywhere, you should look at any of the low level performance minded libraries in the Clojure ecosystem, they are either written in this defprotocols everywhere style, such as nippy, neanderthal, dtype-next, and so on, or mostly in Java, such as http-kit, fast-edn, etc. I noticed this phenomenon, because my own libraries, editscript and datalevin, are written in this way. I take comfort that my fellow performance minded library authors are doing the same. Finally, isn't Clojurescript entirely driven by protocols?
So really, there are two kind of Clojure programmers. One type writes application code or high level libraries, and they write normal Clojure code all the time. However, there are also those who write primarily low level library code, which are used by the first camp, and their code is full of defprotocol and deftypes. So defprotocol everywhere is not anti-pattern. It's anti-pattern only in the mind of the first camp of programmers, and that's a narrow minded way of looking at things. Even the first version of Clojure Programming book by the core team members, are written in a way that's full of defrecord. Remember?
This "everything is a map" orthodoxy is turning people away from Clojure. Just let people write the code that suits their own needs. We can use more people who are pragmatic instead of dogmatic in the Clojure world. If you trust Rich Hickey's judgment, then you should trust him put in the features Clojure has for good reasons. Macros and protocols are part of the Clojure language, and you should be using them when the use case calls for them. Stop the "anti-pattern" nonsense.
These libraries aren't using protocols for type safety though, they're using them as a performance optimization. That's certainly a perfectly fine reason to use protocols, and I agree that it's a completely legitimate use case. It's the whole right tool for the job thing. If you're writing something where performance is the top concern, then that's what protocols are for.
I very much agree with you that that there are people who focus on low level code, and those who focus on application level code. The style of coding will be different depending on the type of problem you're solving. You're right that I failed to qualify my original statement regarding protocols being an anti-pattern.
You're absolutely correct that we should take a pragmatic approach towards using language features. Hence why the context of whether protocols are the right tool to each for lies in the type of code you're writing. And of course, some people find it easier to have more structure to help with their reasoning. Although, I'd argue tools like Malli work better there.
To sum up, I'm not arguing against protocols being useful or that there's no place for them. We started this discussion talking about whether protocols provide equivalent guarantees to Java's type system. I disagree regarding that. However, I also don't think that this is a real problem. Otherwise, use of something like core.typed would've become prevalent by now.
In any case, it is possible to write Clojure in a way that is type checked at compile time. And it is an acceptable way to write Clojure. I just want to clear the air.
If you find protocols are a helpful tool to structure code that's great, keep using them. Nobody is telling you not to. However, try to accept that different people use the language in different ways. Try to follow your own advice and not to be dogmatic about it.
Coders often accept the status-quo of things - "So what that we can't easily upgrade our dependencies in our Node/Ruby/Python project - it's a known thing, we just have to deal with it..."; "Of course our builds take 90 minutes, that's normal for a big project"; "We need complex state management like Redux"; "Code must be recompiled after every change"; "Testing requires mocking everything", "Type safety prevents runtime bugs"; etc.
Yet, when it comes to actual work, not some cheap talking points but real, practical scenarios - there aren't many other tools that can beat Clojure for robustness, pragmatism, and speed of building things with it.
Most experienced Clojuristas are incredibly knowledgeable, skilled, and seasoned programmers, architects, and system designers who are passionate about their craft, using tools, techniques and patterns that enable them to perfect it while remaining pragmatic and grounded. But hey, don't blindly trust my word, go see for yourself - open ClojureTV YT channel, watch their presentations and draw your own conclusions.
Very few languages still in use in the modern era are still weakly-typed.
My personal take is that while a lot of type systems can be overblown, nil-punning is not a sufficient substitute for optional/maybe types.