Hacker News new | ask | show | jobs
by akmiller 2782 days ago
To be fair, I wasn't comparing the two. I've never used Haskell (for more than just learning/tutorials).

I would suggest that if you have runtime bugs popping up in Clojure programs then that would suggest the inputs to functions (since they should be primarily pure) are not being validated which can easily be accomplished. I would imagine this needs to be done in Haskell as well since just verifying types does not indicate valid data.

2 comments

In Haskell designs, developers tend to be quite careful about setting things up so that verifying types does indicate valid data. Coming from other languages (C++, Common Lisp, and Python in my case), it can be a little surprising just how often and how easily you can make this happen.
You can do similar things with spec for Clojure. As I stated in another response that is opt-in obviously so it requires more discipline perhaps but it is definitely available.
Null checks are also opt-in, that's why Tony Hoare calls null his "Billion Dollar Mistake". Explicit runtime checks are in no way "similar" to static typing! Static typing is a machine-checked proof of correctness for a certain class of properties.
I'd rather have the compiler ensure that my discipline (and my team's) never slips, and to get guarantees about all possible executions instead of just those executions that have been tested.

For these reasons and others, I don't personally find run-time checks to be an adequate replacement for compile-time checks. But there is no really convincing research on the subject, and I certainly don't begrudge your preference here.

> To be fair, I wasn't comparing the two.

Well, the statement you were disagreeing with from the post you responded to was:

> You can't refactor Clojure without fear like in Haskell.

You go on to suggest you can achieve a similar experience in Clojure by "depending on how you write your code". This simply hasn't been my experience. Just "writing your code the right way" solves almost every problem that arises in programming, but just isn't always feasible in practice (on a team of developers with mixed skill levels, operating under deadlines, etc).

Re. validation, as Matt Noonan mentioned, in Haskell the goal is often to build/leverage correct-by-construction data types which obviate the need for any validation.

A simple example of this would be the `NonEmpty` (list) type.

If you have a function that pulls a list out of some key in a clojure map and it's intended that it always be a non-empty list you still need to check if actually is or not before using it because you have no control over what the caller passes to you. If the caller never sends an empty list you're fine, but if they do and you don't check for it, you've got a bug. Even if you do check for it, there's often nothing sensible you can do at that point since the local function shouldn't know anything about it's calling context, so you have to raise an error or return a nil or something.

On the flip side, in Haskell instead of using a map you would be likely to create a specific data type, and in that data type you would declare the non-empty field to be of the `NonEmpty` type. The first immediate benefit you get is that you no longer have to do any of these checks for the list being empty (or nil, or something else instead of a list) and instead just write your algorithm over the list. Among other things this results in cleaner, simpler, and less code in your immediate function.

But there's another benefit which is now anyone that calls your function has to have constructed a `NonEmpty` list before they call your function. The impact of this essentially naturally propagates the need to construct that `NonEmpty` list to the right place in the code. Maybe it really is just the caller to your your function that needs to take a regular (possibly empty) list that it has and package it up into a `NonEmpty` to call your function... and in that case you still get the benefits of code that shorter, simpler and more clearly communicates it's intention, but where this really shines is when that requirement to pass a non-empty list makes you realize something about the nature of your problem, and you let that `NonEmpty` propagate all the way out towards the boundaries of your application.

Then you end up in a situation where a) all of the code that touches that field anywhere is simpler, clearer, etc. but more importantly b) if someone does send you malformed data with an empty list (say via JSON over a web API or similar) then the code that deserializes the JSON into the `NonEmpty` will fail and you will get an error that says something like "Couldn't decode a YourCustomType from {whatever it was trying to decode}" at the very moment that the bad data tried to enter your system -- instead of just reading the JSON into a map because it was well formed and then letting the record with the empty list in it bounce around until it hits a function that assumes it's non empty at which point you may have little to no information about the provenance of the data or other details that would make easier to solve the problem.

Exactly, and I was responding to the fact that I never fear refactoring my Clojure code.

Your examples of the type safety can all be mimicked with spec in Clojure. Granted spec is opt-in (but I'm guessing so is some of the more detailed type safety attributes you are talking about like NonEmpty).

Anyhow, to each their own and one persons experience isn't likely to be the same as the others so I'd encourage everyone to try out many languages. Some languages click with people more than others do so it's always worthwhile to experiment.

The opt-in vs opt-out nature is definitely key. As an example, if you write a small function to operate on a non-empty list and use it on a non-empty list in exactly one place, everything is fine in either language. But in Haskell if someone else tries to use the function passing in an empty list, it won't compile, they will encounter the problem right away. In Clojure if someone sees the function and says "oh good, someone's already written this function, I'll just use it" and doesn't realize that it expects a non-empty list, then you now have another situation where something is going to explode when data that contains an empty list arrives. And the error is again going to be far (in the code and in time) from the source of the problem. The difference between the experience of "I have good tools that I can use effectively to arrange to avoid bugs" and "I have tools that simply will not allow entire (large) classes of bugs at all" is what I'm really trying to get at. This is just an area where Clojure shines compared to most dynamic languages but Haskell shines compared to nearly all languages.
Explicit runtime checks that must be manually added are not a substitute for static typing! You should follow your own advice and try out a statically typed language with good inference!

Every tool has a sweet spot and large code bases and maintenance is well outside the sweet spot of any dynamic language.

Well, there are certainly classes of problems that I'd use static typing for, but in my experience, static typing != software that gets the job done. That doesn't mean that it holds things back, though.

Many static typing arguments remind me of the Air Force's old "We'll bomb them so hard we won't have to send in ground troops." I just haven't seen it in practice, and the few studies that have looked at it empirically haven't seen a clear advantage either. If you know of such a study, please point it out!

In the end, all sorts of combinations have succeeded or failed, to the point where now when people start talking about "the right tool for the job", I add in "the right tool for the right people in the right environment for the right job..."

> I just haven't seen it in practice, and the few studies that have looked at it empirically haven't seen a clear advantage either. If you know of such a study, please point it out!

This is basically right; there are not a ton of studies, and the ones that exist mostly have pretty bad methodologies. Dan Luu summarized a bunch of them, circa about 2014. [1]

Since then, there has been one study in this area that I think has a solid, well-defined, and plausible methodology [2]. Plus a "Threats to Validity" section, sorely missing from many other papers in this area. They work from a corpus of real-world public bugs in Javascript programs and quantify how many are detected by simple type annotations via TypeScript and Flow. They cap the amount of time for trying to resolve a bug with type annotations at 10 minutes. The result is that over a corpus of 400 bugs, they were able to resolve about 60 using either of TypeScript or Flow, suggesting that 15% of Javascript bugs can be eliminated by using either type system.

That's not a huge difference, but it isn't trivial either. The authors quote an engineering manager at Microsoft: "That’s shocking. If you could make a change to the way we do development that would reduce the number of bugs being checked in by 10% or more overnight, that’s a no-brainer. Unless it doubles development time or something, we’d do it."

I suspect with languages that are more amenable to static types the results would be even better, but there is no solid research that I know of to back that up.

[1] https://danluu.com/empirical-pl/

[2] http://ttendency.cs.ucl.ac.uk/projects/type_study/documents/...

My gut feeling is the problem matters more than the language. I'm hopeful with the push to microservices we'll be able to mix and match these things where we think they'll work best.