Just to offer a counter-point, I have a Clojure project here with 3k LOC, without using spec/schema. All I have is 700 LOC tests. The tests enforce semantic meaning, along with (some) contracts. I miss types from time to time, but it is no where near as bad as you mention. Against me is the fact is that my app is mostly self-contained, and written all by me. I am fairly certain the project would be atleast 30k LOC if I wrote it in Java.
I think value of types only comes into being when there are many people working on a single code base. Repl/tests/integration tests will take one a long way before it reaches its limits.
I don't buy the argument that types are useful for large codebases, because 1. Types don't enforce meaning. Meaning is far complex than type. Haskell style types only work as far as they enforce meaning. 2. Types have limits too. More accurately, humans have limits. Working with large codebases where there are 1000s of types is hard; which it shouldn't be because that was type systems selling point all along.
Cleaner abstractions of code, separated by strongly enforced protocols(types) is the way to go, I think.
It's probably hard to see the benefits without having tried it first.
This [0] is one of the bigger Clojure projects I've done (around 2.5kloc), also without spec/schema, and I really didn't dare refactor much, even when having a clear understanding of how things could be done better. It was just too much work.
With types I'd go through 10 design iterations before settling for something I'm satisfied it, and even halfway through the project changing things radically isn't a problem (I've worked on a 50+ kloc Haskell backend service, and changing core data structures used pervasively throughout the codebase was a 10min job, literally.)
Top-down doesn't mean that you have to model the universe first before getting to model your problem :)
The type system would be of donkey all use here, because everything has the correct type, with or without the mistake. The code referred to the wrong thing of exactly the right type.
A lot of code works with numerous variables or object elements that are all of the same type, and can be mixed up without a diagnostic.
In this situation, the variable's value was used somewhere, so that didn't go off.
Why do you think this?
> Haskell style types only work as far as they enforce meaning.
Doesn't this contradict your statement above or are you saying Haskell style types never work because they don't enforce meaning?
getUser :: IO (Maybe User)
The above function enforces that getting a user can fail and you must contact the outside world to get a user.Types don't enforce all meaning ie., the enforcement of contracts through types go only as far as they mean something to the problem you are applying it to. It does not cover all the complexities of the problem, or the way the code is changed in the future.
EDIT: This is also why it is easy (and nice) to implement parsers in strictly typed functional languages, because parsers are well studied theoretically. The problems in the real world are not studied well enough for contracts enforced via types to work completely.
That seems sort of backwards. It enforces that the caller be able to handle failure (and similar for IO). It may well be that "getting a user" doesn't do either (e.g. `pure (pure defaultUser)`)
Could you go into a bit of detail about this approach? Do you mock functions out and just specify type definitions, and fill in functionality as you go?
Note that src/Hotep.hs exports a bunch of undefined functions. I've been able to verify that these types all make sense even without implementing a thing. As time goes on I may learn that the implementation drives the types somewhere else and then the compiler will make refactoring easy.
However, already I've gone through about 5 iterations of this design which drove me to debug some structural questions about the whole affair and also dive deep into Erlang documentation to determine how they solved problems. These explorations and their results are encoded into the types.
At this point I'm beginning to consider implementation and I can keep filling out just partial implementations against these types. I'll probably make 2 or 3 toy implementations to test out the ideas again with more strength before moving on the final ones. The whole time the types will be guiding that development and helping it move quickly.
Key to this whole affair is the need to describe types utterly before a completely successful library is made... and also the ability to defer the burden of providing type-checking code for as long as desirable. Haskell supports this wonderfully—even more wonderfully with things like Typed Holes and Deferred Type Errors which enable a really great interactive experience I haven't yet needed to employ.
This seems to be a widespread sentiment.
In practice, I've found that prototyping anything remotely complex without types is so painful that I'd rather settle for an inferior design than trying to come up with the best possible abstraction.
In Haskell, I come up with a coherent skeleton without having to implement mundane details, hit a wall in the design space because of some case I didn't think of, come up with a better idea and then go back to the code and refactor with confidence. The type system always guarantees that my prototype is coherent as a whole. And I can do that dozens of times.
In Clojure, even with spec, I'd have to implement all my functions fully before being even able to test the design as a whole (sure, testing individual functions works fine in the REPL.) And after hitting a wall, having to reimplement everything every time is just too much work.