8 ms·
I'd disagree with this. Primarily been developing with Clojure for 5 years now with some pretty large codebases. It does depend on how you write your code but
by akmiller 8y ago
I'd disagree with this.
Primarily been developing with Clojure for 5 years now with some pretty large codebases. It does depend on how you write your code but favoring pure functions, pushing immutability to the edges of your programs allows you to refactor without fear.
Clojure also has many things to aid in this such as pre/post conditions, clojure.spec (which allows you to build complex type definitions), and of course test.check (property based testing).
- lgas 8y agoHaving spent the last two years working professionally in Haskell after having spent the previous two years working professionally in Clojure, I'd disagree with this. Haskell is roughly infinitely better than Clojure for meaningful refactoring. Clojure gives you (great) tools for using your own brain to make sure the refactor goes well. Haskell replaces your brain almost entirely in the process and just gives you a precise list of the things have to fix. The difference in experience is night and day. When you're done with a Haskell refactoring (even a major one) and it compiles again you will be completely done 99% of the time. With Clojure there's not even a "compiles again" moment to look for, and even when you think you're done you're refactoring there's a good chance that there's something that will pop up at runtime (and some point arbitrarily far in the future) and tell you otherwise.
- akmiller 8y agoTo 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.
- matt-noonan 8y agoIn 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.
- akmiller 8y agoYou 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.
- willtim 8y agoNull 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.
- matt-noonan 8y agoI'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.
- lgas 8y ago> 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.
- ken 8y agoJust as with the word "developer" [1], I'm beginning to think that the word "refactor" is a dirty word. Are we even talking about the same activity? How is it people are alternately claiming that Haskell and Clojure (virtual opposites on the language spectrum) are infinitely superior to each other at the same thing? Wikipedia defines it as "the process of restructuring existing computer code without changing its external behavior", which sounds about right, but it's also so completely generic that it could mean almost anything. Claiming that Clojure can't do 'meaningful refactoring' sounds to me about like claiming that Kanji is bad for transcribing Welsh, or that sign language doesn't work well for audiobooks. They're technically languages but the fundamentals are so different that all the comparisons are talking right past each other. The style of refactoring that one does in Haskell (disclaimer: it's been many years since I've written any) is not really possible in Clojure, but it's not really necessary, either. [1]: https://news.ycombinator.com/item?id=18310958 https://news.ycombinator.com/item?id=18310958
- lgas 8y agoI never claimed that Clojure can't do meaningful refactoring, and in fact I acknowledged that Clojure gives you great tools for using your own brain to do refactoring. Clojure is my favorite dynamic language and I heartily recommend it to anyone looking for something in that space. It's just that the experience of refactoring (or anything like it -- even just making large changes) in Haskell is massively better than the experience of doing it in Clojure.
- wild_preference 8y agoI'd say it comes down to the more standard fare of dynamic vs static typing where the latter has a general advantage in refactoring. It's true that Clojure has its own perks that aid in refactoring, like how you may have less code altogether. But in my experience it's still a drop in the bucket compared to the zoomed out view of dynamic vs static typing. For example, could you give some examples that make Clojure particularly good in this regard? I'd have trouble coming up with many that quantify favorably against compile-time analysis. And it would be unfair to take this reality and say "Clojure sucks at refactoring." I think that people do say that is what clouds these discussions where one then needs to point out that Clojure has advantages over other dynamically typed languages which is definitely true.
- tanin 8y ago> It does depend on how you write your code but favoring pure functions, pushing immutability to the edges of your programs allows you to refactor without fear. Dynamic typing is punitive if we make mistake (e.g. using the wrong architecture, now we have to refactor), and we'll make mistakes. Writing perfect code with perfect architecture and perfect test has been the argument "for" dynamic typing, but I don't think that's a realistic assumption.
- yogthos 8y agoMy experience is that static typing tends to lead to higher coupling and monolithic design, while you tend to break things up more aggressively in a dynamic language like Clojure.
- tanin 8y agoI understand what you mean, and it's definitely a trade off. Static typing requires all types to make sense together no matter how small the addition is. In dynamic typing, we can just add a class/object/field here and there because it'll only be used narrowly anyway. So, it's often fine. This would be great if we can think of a small example to illustrate this trade off.
- yogthos 8y agoOne example I can think of is how Ring middleware works. The request map is passed through a series of functions, and each one can modify it in its own way. For example, you could have an authentication library that adds an :identity key, and another that adds a :session key. These libraries can work together without having to know anything about each other.