Then I tell them about strings and ints. You can represent 66 both with strings and ints, but one is usable for computation where the other one is simply usable to write Alice in Wonderland with it, or simpler forms of text. Sometimes I get the question then: why can't a string both be computational and as a means of character display? And then I list the upsides and downsides of such a system, just like the upsides and downsides of mixing brick and wood to build the outer wall of a house.
I'm curious if people can think of other analogies that they use for teaching.
Personally, I avoid analogies because they usually mean I don’t really know how to teach the topic, and I’m “hand-waving” on the fly. I either find a way to build on the student’s existing knowledge or I “park” the topic for discussion when the student has enough knowledge to give a correct answer.
Run what you have so far. Print the last value you computed. Crash. Verify the data has the "shape" you expect it to. Write a bit more code. Repeat.
On the other hand, Python’s repl was nice for a small handful of tasks (although I mostly used it for figuring out what the actual type of some variable was, which is obviously a non-problem in the statically typed world).
We get editor time feedback of mistakes with optional type hints + light inferance via clj-kondo and a data modelling system that we can export out as database schema, JSON schema etc
And dynamic enough constraint system to express something like all human names must be "Tony" only on Tuesdays
The same constraint that can be shared server side and client side without writing it twice without learning more syntax
Additionally check what the expected inputs for a function are by checking the function specs I think guardrails pro will make this more ergonomic when released
And finally ask the constraint system to generate valid examples of the constraints great for mocking data
I don't miss type systems but I also understand if you're not using solutions here then you're in trouble
Clojure is a good example here, it actually can be used with a very powerful static type checker core.typed, yet its users chose not too for reasons that say in JavaScript maybe a different choice would have been made. The REPL for example catches lots of type errors as you code. Other languages don't have such a coding workflow, so a static type checker feels really great in that it too will catch type errors as you code, etc.
None of that is to say OCaml is "superior" to Clojure in some way. I disagree with a lot of the ways that the OCaml type system has evolved and I wouldn't be surprised to see people who have moved to Clojure from OCaml. However, having programmed professionally in Clojure (although it's been quite a few years so I'm not familiar with the latest advancements in e.g. spec) I still think a Clojure-like language could benefit from a static type system.
I don't think it'll work for Clojure itself because of a variety of patterns and choices in the standard library (which is in part why I think core.typed died, we also found core.typed painful to use in some of our experiments at my old job both in how it interacted with Clojure and the tooling around it). And philosophically Rich Hickey would probably kill Clojure before he ever considered designing Clojure around a static type system. However a programming language based off the same data driven ideas could maybe do it.
While a REPL and hot code reloading are absolutely huge productivity boosts, they are more or less orthogonal to the benefits provided by a good static type system (see e.g. hot code reloading with Elm or Purescript which comes quite close).
The thing that static type systems provide over tests and runtime contracts is the ability to constrain users of the API of a library. We use regression tests to make sure regressions in code we write doesn't happen again. Likewise types are effectively regression tests at the API level to make sure certain regressions in code that calls our code doesn't happen again. That is an extremely powerful capability that I consistently miss in dynamically typed languages.
That's what I find interesting about it. Not all language benefit from a type system in the same ways, some, like Clojure, actually get crippled. Now, it can mean that you need to find the right kind of type checker that provides the correct ergonomics for Clojure and maybe that would work. But it's still quite interesting.
For example, Erlang has a bit of a similar thing, Dyalizer made specific choices to work within Erlang's design. Had it not done so, it probably wouldn't have found adoption. Same with TypeScript.
So what's interesting here is that you have an apples to apples comparison where a language is found to be better without the constraints of static type checking.
When you look at other statically typed languages, they're oftened designed around the static type checker. That's the main focus, and the language itself revolves around that. So obviously in such a language, the type checker would be a necessity, as it's the main draw. So it's interesting to look at Clojure for a counter example.
That said, JavaScript you could argue is also an apples to apples comparison, and people opted for types. It'll be interesting to see also where Ruby and Python go, now that they have type checking features as well.
You can go crazy and spec everything, in fact that can help, but:
- In practise, nobody does it
- Specs come with no guarantees. They could even be wrong.
- The official implementation stubbornly insists on not checking return types, so half of your annotation may just be glorified documentation (although you can use third party libs like orchestra)
Just imagine: Add a new required field to a spec, and get a convenient list of source code locations that you need to review. That's the promise of a statically checked system. It's not a silver bullet, but not having this leads to what I like calling "refactor anxiety" (i.e.: did I handle all cases?)
I still love clojure no matter what. I think in practise you can express so much, so elegantly, and with far less code, that your project size is always sorta manageable.
Ultimately I think people just weren't given any credit for making good, reusable types, because then the next dev who submits a better feature faster using your work gets a raise, but you look like a kook ranting about best practices who doesn't do anything "business".
Bad type systems need you to use the escape hatches frequently - you can't write much C without using casts.
I haven't yet used a language with no need at all for an escape hatch - but some languages need them far less often than others.
Java's type system is better than it was. The main places it still has weaknesses are around exceptions (you will need to wrap checked exceptions in runtime exceptions in places you'd have preferred to specify an exception type via generics, eg) and the occasional cast after an instance type check.
You can phrase good development practices in business terms: what are the risks to the business due to sloppy code? Are they greater than the risk of being slow to market?
Sloppy code has accumulating costs. A good type system can help greatly with refactoring to address those costs, but it cannot help with the attitude that those costs aren't real and don't need to be paid.