1. 9
  1. 14

    People complain all the time about how verbose the Java type system is. I mean, what about this kind of code:

    List customers = new ArrayList();
    

    Do I really have to say it twice?

    That’s a disingenuous example. Java gained support for the var keyword in March 2018 with the release of Java 10:

    var customers = new ArrayList();
    

    What’s more, one might meaningfully choose to write List over var to communicate that the rest of the code should not rely on the unique behavior (performance characteristics or unique methods) of the ArrayList as compared to other List implementations.

    A lot of people (myself included) argue that the Java type system gets in the way more than it helps. […] You give up the small amount of safety for a huge amount of flexibility.

    Given that I just refuted the only example the article gave of a problem with Java’s type system, I think this argument needs expansion. I recognize that Java’s type system has limitations, such its single-dispatch that necessitates the cumbersome Visitor pattern, but I’m not convinced that a large codebase would be better off sacrificing Java’s compile-time type safety to use Clojure. (Of course, other statically typed JVM languages might be even better than Java.)

    1. 11

      Another feature that Clojurists like to claim as a feature is Clojure’s nil handling and I have to strongly disagree.

      Yes, you avoid a NullPointerException where you try to use a nil value, but instead nil quietly continues propagating through your program, being part of maps, transforms of maps and at some point something falls over because the value that you expected is nil but that’s not a valid value, so you have to trace the entire program back to where the nil actually originated and see why that was wrong and fix it in that place.

      This combined with the fact that it is way too easy to get a nil in regular Clojure code (because getting a non-existing value out of a map will give you that unless you insist on a sentinel) makes it basically the Clojure equivalent of the infamous “undefined is not a function” error. You can be extremely defensive and to Go style “if error” checks (which due to Clojure’s macro system would be more bearable than in Go), but I really don’t think this “on error resume next” strategy is a strong point of the language.

      1. 4

        I really like Clojure and I think its reliance on nil punning is absolutely one of the most boneheaded “features” of the language. It feels like it was designed exclusively to look good in a show floor demo, but spending just a little time with it makes you realize that it’s error-prone in the ways that you mention and it isn’t semantically illuminating either. I chafe every time I see or type the idiomatic (if (seq collection) true-case false-case) to test whether collection is empty, because I get the feeling that someone thought it was a cleverly elegant way of expressing that condition. It’s not. It’s really, really not.

        1. 4

          I really like Clojure and I think its reliance on nil punning is absolutely one of the most boneheaded “features” of the language.

          +1 to that; the nil punning is definitely one of my pet annoyances with Clojure. However, it seems that many people feel differently. It inherited the nil punning from Common Lisp, where it’s also pretty (har har) common to nil-pun (for instance, (car nil) returns nil). But another problem is that Clojure doesn’t go all the way. There are several builtins that simply forward to a native call and those will raise a NullPointerException. There’s not really any rhyme or reason to it, you basically have to know which ones will pass through the nil (most will) and which will not (some surprising ones will not, for example most of the string functions, even though strings can be seen as collections).

          (if (seq collection) true-case false-case)

          I never got why that was considered idiomatic either. How is (if-not (empty? collection) true-case false-case) any less readable?

        2. 2

          I’ve been working with Clojure for around a decade now and nil punning has never been a real problem in practice in my experience. What actually happens is that you just let the nils flow through the code and you do validation at the edges. Nowadays, you’d also use Malli or Spec to define the schema at the API level. If you’re nil as an error then you’re not really writing idiomatic Clojure.

          1. 1

            This is the canonical answer that many issues with Clojure get: “you should’ve done it differently”, which while obviously true, I should just not have created bugs, but practically not helpful.

            I would rather see these things being acknowledged and tackled rather than played down. To me this is the canonical “I can run with scissors, as long as I am careful not to fall with scissors (or have prepared cushions that if I fall I don’t hurt myself)”. Not specific to Clojure itself, but I have found the Clojure community to be extremely insisting that that is the right way (imagine Python users all saying that having a GIL is Good Actually™).

            Yes sure there’s ways to deal with specific issue, like use a library that will only be invented in the future like Malli (2019) or Spec (1.9, 2017, still called “alpha”) which both came pretty late to the Clojure (2009) world and Schema which came earlier (2013) but is dead now but if the solution to this is validation of all intermediate maps then Clojure is missing a convenient language element that guarantees that keys are actually set. On the other hand, if the validation is only at the edges, you only get to know about it when it the validation fails and then you still have to trace back where that nil originally came from.

            And sometimes you just have a codebase that’s written this way or other and it generally works so you can either trace up where you accidentally created the nil and fix it or spend many hours of rewriting and adding validation everywhere (bonus points if this is a library that is not written by you, but rather FOSS code where contributing large patches is always a somewhat painful exercise, even if the maintainer is very active).

            1. 2

              Every language in existence has its own patterns and best practices. So the argument that Clojure practices are different from what you’re used to isn’t terribly convincing.

              Different languages make different design choices that have their own trade offs. This is of course the case with nil punning in Clojure as well. However, as I’ve already noted, I haven’t seen this choice manifest itself as an actual problem. If you’re claiming that it is then please show me examples that it is in fact a source of errors. For example, we could look at Github issues for popular Clojure libraries and see how many of these issues trace to nil punning.

              What you’re doing here is making an utterly unsubstantiated assertion that nil punning leads to errors. Then you take this assertiion and treat is as a fact without providing a shred of proof to support the notion.

              1. 3

                What you’re doing here is making an utterly unsubstantiated assertion that nil punning leads to errors.

                I would say it doesn’t lead to errors, it just makes debugging much more difficult by carrying on with an unexpected value until it crashes at some later point, where you may have no idea where the nil came from.

                1. 1

                  Clojure development is typically done interactively by connecting the editor to a REPL. My workflow is to continuously run code as I write it and I always know exactly what it’s doing. For example, let’s say I need to get some data from the db, massage it in some way, and send it to a client. I’d write a function to pull the data, run it, look at the data I’m seeing. Next, I’ll pass it to the function to transform it, and look at the result there, and so on. At each step I only need to worry about the current transformation I’m making and the state of the data in the last step.

                  As I’ve mentioned in another comment, thinking of nil is a wrong value is not the right mindset here. A better way to look at it is that you’re querying into the data structure and you may or may not get a result.

                  1. 1

                    In my experience, you tend to be a lot more “rough” about what you try out in the REPL. Typically those are couple of variations of the happy path. This means you won’t be testing what happens when some part of the input you’re expecting to mostly be there will be missing (thus yielding nil).

                    Besides, the point was that it’s difficult to debug, which you’ll usually be doing after a report of something happening on production. Or very complex code in development where certain inputs yield nil somewhere along the way.

                    1. 2

                      Any real world application is going to have some sort of regression testing which will catch these kinds of problems. Meanwhile, I’ve been maintaining large apps in production for many years now and your assertions simply doesn’t ring true to me. It’s generally very easy to tell where the problem is based on the stack trace. Vast majority of the time when you see a production issue, the problem will be within a few lines of where the trace leads you.

                      As I’ve already explained, nil should be treated as an expected result, and it’s handled sanely by standard library functions. Meanwhile, your complex functions are just combinations of these functions from the standard library. So, what actually happens is that nils just flow through the execution chain, and then you decide how you want to present them at the edges. This is also where you’d typically use things like spec to do validation and coercion of data.

                2. 1

                  Have you ever had any problems with Clojure?

                  1. 2

                    Sure, I don’t think there’s a language in existence that won’t give you problems. A few pet peeves I have would be how cons and conj work, the fact that some core functions fail to handle NPE when wrapping Java calls, hot loading of libraries being flaky (although this is more of a JVM problem), and poor REPL experience compared to CL. However, nil punning is simply not an actual problem that has any real negative impact on development or code quality.

                  2. 1

                    Every language in existence has its own patterns and best practices. So the argument that Clojure practices are different from what you’re used to isn’t terribly convincing.

                    The other language I can think of where it is somewhat common to give unexpected answers to wrong input data instead of failing is JavaScript, where it is not considered a good part of the language.

                    You’re making the assertion that is it not a problem by stating that it hasn’t been a problem for you personally in a decade (I’ve also been using Clojure on and off since before 1.0 and it being my favourite language for some time, but so what?), but even people in this thread have mentioned issues with it.

                    I can’t point at specific examples because the examples where I spend a lot of time debugging were in proprietary code bases where you often get to correlated multiple domain “objects” together to form new domain objects. I’d say this is less of a problem in popular Clojure libraries since they’re often wrappers around Java libraries, with a lot less complex data being shuffled around in maps.

                    1. 2

                      The other language I can think of where it is somewhat common to give unexpected answers to wrong input data instead of failing is JavaScript, where it is not considered a good part of the language.

                      Except that Clojure gives expected answers. As I’ve already pointed out, the only issue here is that your expectations don’t match how the language works. The way to think about it is that you’re making a query into the data structure, and the query either returns a value when present or nil when there isn’t one. This isn’t an expected answer.

                      You’re making the assertion that is it not a problem by stating that it hasn’t been a problem for you personally in a decade (I’ve also been using Clojure on and off since before 1.0 and it being my favourite language for some time, but so what?), but even people in this thread have mentioned issues with it.

                      I’m saying that it hasn’t been a problem for me personally, it hasn’t been a problem for other people I know working with the language, and I haven’t seen it be a problem in many libraries I use and have contributed to over the years.

                      I’d say this is less of a problem in popular Clojure libraries since they’re often wrappers around Java libraries, with a lot less complex data being shuffled around in maps.

                      There are plenty of Clojure libraries that do lots of complex data manipulation. I work in medical domain and I have to work with lots of complex data such as FHIR all the time.

          2. 7

            A better syntax (debatable) isn’t worth losing static typing.

            1. 1

              My experience is that immutability plays a far bigger role than types in addressing this problem. Immutability as the default makes it natural to structure applications using independent components. This indirectly helps with the problem of tracking types in large applications as well. You don’t need to track types across your entire application, and you’re able to do local reasoning within the scope of each component. Meanwhile, you make bigger components by composing smaller ones together, and you only need to know the types at the level of composition which is the public API for the components.

              1. 2

                You don’t need to track types across your entire application, and you’re able to do local reasoning within the scope of each component.

                This is the complete opposite of my experience. How is one supposed to locally reason about a function if you don’t know the types it operates on?

                1. 1

                  You only need to know what the shape of the data a particular function expects. Generally, people document the shape of the data a component expects at the API level, and it’s often done using spec or malli contract systems. The advantage of using contracts is that unlike static typing they actually capture semantics of what the code is doing.

                  Out of curiosity, how much experience do you have working with large Clojure projects?

                  1. 2

                    I implemented a ~10k LOC service (specialized in memory cache) in Clojure back before spec was a thing (11 years ago I think?). Prior to that, I only had hobby experience with CL. It was fun to write but hard to maintain.

                    1. 1

                      11 years ago there really wasn’t much to help out with tracking types. The first library I can think of that allowed describing the shape of the data would be schema, and that came out in 2013. I also find that code style plays a big role in maintainability.

                      I’ve seen beginners write fairly impenetrable Clojure before, so if you were new to the language and working on a sizeable project I can see how things got messy. I’ve written some code I’ve regretted myself as well. My approach was to pay attention to the parts that I had trouble maintaining, and to adjust my style to make my code easier to follow.

                      I do agree that it’s easier to make a mess using a dynamic language and you have to be more conscious about breaking things up. On the other hand, I find that statically typed code can get differently impenetrable. I’ve worked with Java for around a decade before I started using Clojure and I’ve seen some incredibly gnarly code. I’ve also never managed to contribute to a single open source project. Any time I’d open one up and try to add a conceptually simple fix, I’d be overwhelmed by all the classes and indirection. On the other hand, I’ve contributed to numerous Clojure projects and many people have contributed to projects I maintain.

                      1. 1

                        I don’t think the overabuse of massive class hierarchies is an issue with static typing (given many statically typed languages aren’t even using objects), this is more a problem with Java where a class/object is/was essentially the only abstraction mechanism so everything has to be forced through this. Any language (like Clojure) with first-class functions can avoid a lot of the headache.

                        1. 1

                          Types encourage coupling by virtue of types being global constructs and I suspet that’s why projects tend to be developed in monolithic style in statically typed languages.

            2. 6

              The original post is pretty weak, but I’m glad to see so much lively discussion in the comments. Reminds me why I keep coming back to lobste.rs!

              1. 1

                Same here! I hadn’t seen this article posted on this site yet, so I figured I would take the liberty and see what other people’s thoughts on it are

              2. 4

                As a long-time Clojure and Java programmer, I don’t agree with Eric here. Clojure is really just a Lisp targeting the JVM. If you like Lisp and you’re in a context where you can make use of the JVM (which is more places than I thought that would be a decade ago), it is an absolutely fantastic language. The combination of the most powerful language family on a ubiquitous runtime has lived up to its promises. I still prototype ideas in it even if I’m not going to use the code in production.

                But Java is still very relevant, particularly for team-maintained large codebases, or where you really need to just write Java and Clojure is only adding noise. Good Clojure can be written and maintained by a large team, but it takes above-average Clojure programmers to do so and almost by definition you can’t have a large team (>10) of above-average Clojure programmers.

                1. 2

                  I don’t really follow the argument about large teams to be honest. My experience is that the problem with large teams has little to do with the choice of technology. Large teams introduce a lot of communication overhead, which makes for longer meetings, more emails, and more confusion where left hand doesn’t know what the right hand is doing.

                  Any large project can and should be broken down into smaller projects maintained by small teams. This is basically why microservice architecture has become so popular of late. Breaking things up is the best way to ensure that projects are maintainable regardless of the choice of language.

                  All that said, I do think that Clojure tends to work best when you have mostly experienced developers working on a project. It’s also not a great choice if you’re doing a lot of rapid hiring where you’re going to have to train a lot of new devs on the language. So, Clojure will never replace languages like Java in corporate environments.

                  1. 2

                    I don’t really follow the argument about large teams to be honest.

                    I should probably have used a “large codebase,” which is more likely to need a large team supporting it. But you’re right, it’s not necessarily true. And that’s much more likely to occur with Java both because it’s a verbose language, and static typing enables it.

                2. 2

                  seeing that doto operator reminded me of the Javascript with block, which I fondly remember as the thing people told me I should never ever use. But it always fascinated me as a concept, and honestly it doesn’t feel that much worse than languages with implicit this/self in methods.

                  Of course the clojure thing is way more principled

                  1. 8

                    The danger of JavaScript’s with is that it applies to all variable lookups within the block, allowing confusing code:

                    let obj = [];
                    obj.x = 123;
                    
                    let x = 'push-me';
                    with (obj) {
                      push(x); // calls obj.push with 123, not with 'push-me'
                    }
                    

                    Clojure’s doto doesn’t have this problem: it only inserts the passed scope value at the top level of the arguments, similar to if JavaScript only modified the beginning of each line within a with.

                    1. 2

                      It’s possible that the advice to not use with is outdated, though I don’t know for sure, just speculating.

                      1. 4

                        The reasons to not use with in JavaScript are still relevant. See my sibling comment and the additional examples of ambiguous code on MDN.