Clojure is cool
ahungry.com
ahungry.com
But in addition to that, shadow-cljs is truly incredible, kudos to Thomas Heller. When developing, my editor is connected to the browser app through a repl, and I can switch namespaces and sort of TDD new code, or debug an issue by cracking into the actual pipeline and interactively spelunking. In JS land, everything is transpiled, so really you have to put debuggers, refresh the page or action, catch the debugger, and do stuff that way. If you're developing new code you can't test out a function if it uses some transpiled feature, so you write a test or do the debugger thing.
It's takes some dedication to get there, and you don't need emacs even though it's really fun to learn and get proficient in. I use Spacemacs and am constantly learning some new package that's installed to help me. I recently switched from parinfer to paredit, and it's reaalllllly cool. With the repl driven development and structural editing you can achieve this kinda mind-meld with your development process. I don't think that necessarily makes this better than Javascript, but if you're into stuff like that there's a really high skill-cap with how you can optimize your development workflow.
And really, at it's core to me Clojurescript is like my perfect Javascript. There were not any new concepts for me to learn as I had been programming Javascript functionally for some time, it's just everything I wanted in Javascript without the friction and bolt-on libraries.
For developer happiness, it has a ton to offer, and there's always something else to dig in to.
Clojure:
(+ "1" 1)
ClassCastException java.lang.String cannot be cast to java.lang.Number clojure.lang.Numbers.add (Numbers.java:128)
Clojurescript:
(+ "1" 1)
"11"
They may be close, but that is all the reason for concern. There are a million ways that small semantic differences like this can completely fuck you and leave you in a debugging nightmare. I would rather use javascript, AKA the worst language ever invented, than a language that claims to be cross platform but with semantics that change depending on the platform.
cljs.user=> (+ "1" 1)
⬆
WARNING: cljs.core/+, all arguments must be numbers, got [string number] instead. at line 1
"11"
You can also use Spec and Schema to validate data at the edges, so that you don't end up with unexpected inputs. I highly recommend doing that for any non-trivial projects.No, it does not. The same Clojure code may be the source for both execution environments, but that doesn't mean that the code that runs (JVM bytecode or JavaScript in the browser) is remotely similar, follows the same rules, errors under the same conditions, or otherwise behaves similarly.
That's not an argument against code sharing. But please read the post you're responding to thoroughly before you post "I can write it once and run it anywhere" without a whole lot of asterisks after "anywhere".
Still, you get immutability by default and a nice std lib of functions, think of lodash/fp and immutablejs together but better since lodash and immutablejs can't be use together anyway.
than a language that claims to be cross platform but with semantics that change depending on the platform.
This is not true, on the contrary, Clojure[script] claims to embrace the host semantics and platform, is just that there is this capability to share some code between Clojure and Clojurescript that might be useful for some parts of your application, I don't think this is a killer feature of Clojure btw.
Yes, of course there are common bits that can be factored out. The problem is that most people have never so much as thought about it, because of the language barrier, and other barriers – for example teams are structured around this artificial divide of frontend and backend. You need a new architecture to take advantage of the parallel structure. GraphQL is just the beginning.
However, when backend and frontend code exists in the same context for analysis, aren't there enough certainties to know the space of possibilities for what you're going to receive? How much uncertainty precludes the useful sharing of types and methods?
A common use case for the shared code is manipulating the application's domain data structures. Sharing the code for these manipulations is routine. In SPA apps you frequently do more than just render views for the backend, and then you typically want to have the backend's internal representation of the data at hand even at the frontend.
You can call it a library use-case of course, depending what unit of modularity you choose to call a library. But this way you don't have to update 3 projects for each change (app-backend, app-frontend, app-shared) and you can very flexibly refactor stuff into the shared namespace without breaking the normal flow of development.
If you are not working for a startup, majority of the time, you will be maintaining legacy code or bug fixing. I would take easily understandable verbose code over "clever" concise code every time.
And in the general case verbosity and cleverness are orthogonal properties.
You would think so, sure. I used to write Perl code and I was extremely familiar with it and it used to be pretty easy for me to hack a script. Going back after couple of months and trying to understand it though, was a totally different animal.
You can argue that it was my fault for writing bad Perl code, but ask any old farts who ever had to write Perl.
Is there some particular aspect of the Clojure code here that you think is overly clever, or hard to understand? This Clojure code uses only one lambda, and in a straightforward way.
I've written a lot of Java, and a moderate amount of Clojure, and if I had to place a wager on which version had fewer bugs, I'd definitely bet on the Clojure. Especially if there were the possibility that it was related to threads.
We could write this in assembly language, and it'd take 50,000 lines, and probably have lots of bugs. The salient point is not (just) the lower line count, but that when code is shorter, that's a good indicator that it's written at a level of abstraction that fits the problem.
"Code Review Metrics". [0]
"A Large-Scale Study of Programming Languages and Code Quality in Github". [1]
"Software Quality Metrics". [2]
"Study on the Correlations Between Program Metrics and Defect Rate by a Controlled Experiment". [3]
0 - https://www.owasp.org/index.php/Code_Review_Metrics
1 - https://cacm.acm.org/magazines/2017/10/221326-a-large-scale-...
2 - https://www.developer.com/tech/article.php/10923_3644656_2/S...
But if bug count is proportional to LOC and LOC per day are constant across all languages, then bug per day will also be constant across all languages.
You can write shit code in any language. I used to think Java made it harder to write shit code, but the project I'm on right now has made be reconsider this opinion.
Of course -- but the important thing is that bug count per feature/app will be lower in a more expressive language.
Umm...so for the same amount of bugs I can have more features if I choose the more expressive one...
IME, the bugs are also easier to deal with in the more expressive language. They tend to be things like faults in the business logic or gross edge cases that people are likely to catch in code review or QA. Whereas the bugs in languages like Java seem to typically be really annoying things like off-by-one errors, comparing Integers with ==, and goofy run-time type errors that sail past the compiler because of weak static type checking when generics are at play, and also past code review because people aren't expecting to have to review for type errors when they're using a static language.
Yes, I'm inclined to agree, but I actually haven't ever seen a study which compares the same project implemented in different languages to establish in toto the variance in SLOC. It could be the case that in the main for the same project the differences between languages wash out, as different languages may have different advantages and disadvantages that are more likely to tell on a substantive project.
What you suggest seems reasonable, but I simply don't know it to actually be the case.
I don't know about that, a lot of people seem to like j[1]
Sure, you can trade off static typing for terseness. This isn't a Java vs Clojure issue. You can make code even terser by abandoning documentation. Even more terse by abandoning tests!
I don't have time to fully dive into the second example, but at first glance it looks like poorly written Java. You can write Java in a functional style! It can look a lot like Kotlin or Scala or even Clojure, and I generally prefer it to.
Badly written code looks terrible in every language.
Clojure code is far easier to maintain for a number of reasons. The code is declarative, so it separates what's being done from the implementation details. The first step of code maintenance is to understand the intent, and it's much easier to do that with declarative code. Immutability means that the code is largely referentially transparent, so the cognitive load of understanding a particular piece of code remains constant as the project size grows. This is not the case for imperative languages where you pass references to shared mutable state all over the place. The syntax is much smaller and more consistent, meaning that you have to learn less rules and quirks to understand the code. There are less chances of code being misinterpreted. Finally, you have the REPL, so you're able to run any code you're not clear about right from the editor in the context of your application.
My team moved from Java to Clojure about 8 years ago, and we find that it's much easier to maintain Clojure projects than it was for similar scope Java projects. We deliver faster, we have far less defects, and we're able to make changes much more reliably than we ever could with Java.
Could it just be that you are all better developers than you were 8 years ago, and the language doesn't really make a difference?
Java 8 was a special version. Lambdas, stream API, diamond operator and new exception catching syntax together eliminated 50-80% of noise in Java code. The language is still verbose, but much less than pre-8.
(Still, I prefer reading Clojure. The form of Java language still causes too much structural noise in the codebase.)
Most actual surveys put Java 8 penetration at 70 - 80%. Dig deeper and the <20% of projects not on Java 8 aren't under active development and are purely in maintenance mode.
This is what makes the entire exercise a myth. People may want to believe this stuff but again it has no practical basis.
I was pretty much spearheading the use of Java 8 in one company ~2.5 years ago, but I know some teams actively developing there only upgraded a year ago, and I'm willing to bet the main customer still didn't...
The myth of the myth of Java code only serves people defending Java academically, and those hung up on defending a language that enourages and defaults to bloat in any sort of shared coding environment. It doesn't seem to have any practical basis.
The fact, that people come up with such code very often is the general characteristic of modern IT job market, where demand is so high, that it eliminates all possible qualification barriers. This particular code is written so not because Java is too complicated, but because the author did not know the minimum required for professional software development on this language or have written it this way on purpose.
Maybe the clojure REPL should be renamed to something else to differentiate it from other language REPLs
Comments are fine when the code is doing something unexpected or that is very terse. In other scenarios, comments are just a land mine to be armed when the code changes and the comment isn't perfectly updated. Bugs largely come from developer expectations being broken (mostly by one of: misunderstanding data shape, some API detail, some language feature, or miscommunication on the feature with the owner) and stale comments are a contributor to this which can be avoided in many cases.
Docstrings for trivial, well-named functions just get in the way, and if you feel the need to add a docstring to a complex function just after you've defined it, then I think that might be a hint that you should refactor the code to make it more clear and readable instead.
An ns docstring that describes the intent of the api and docstrings for the major public interface functions are a good idea, but a blanket "docstring for every function" rule is a crutch to make up for unreadable, poorly-written code.
Comments are a useful for explaining the implementation logic of a function or method, for instance. Or for generating API documentation. And file-level comments explaining the purpose of the class or module are always handy.
IMHO structure editing emphasizes the wrong part of code development. In practice collapsing graph structure is less important in code as opposed to, say, JSON. Whereas you want to be able to use your eye to hop around, and free-form alignment sometimes helps to show parallel constructs.
Remember also that in a structure editor even your comments have to be part of the code and can only be inserted in places where an expression won't change the flow of control (e.g. you can't do (if (* this is a comment*) some-condition result))
Quite the opposite. Common Lisp macros are totally unrestricted, they don't auto-qualify names with the package. Also, there are user-defined reader macros in CL, unlike Clojure.
In a very OO language, where you're encouraged to create a complex type for every function/method contract (i.e., I have a type of RoomMeasurement, that internally contains a list of Measurement interfaces, each of which is in fact implemented as a MeterMeasurement, which wraps a double), static typing is very, very necessary, because it's not at all obvious what a function takes. And you need thorough API documentation because how another developer has chosen to represent things is not obvious (that is, you are trying to interoperate with a library that doesn't understand your RoomMeasurement, but does work with just a list of measurements, but they have to be in imperial, not metric, and how do you get the list of measurements from your RoomMeasurement, and convert them? Do you have to write a function, is there one already, does it take the RoomMeasurement, does it take the list of MeterMeasurement, does it just take a single MeterMeasurement? Etc)
When sticking with simple types, though, it becomes a lot easier to reason about, and you can get away with just comments, or very slightly more complicated types. A list of measurements is just a list of ints...but maybe dropped into a tuple where the first arg is the type (i.e., roomMeasurements = {meter, [4.22, 5.7, 3.1]} ). And all you have to find/write (and since it's just data it doesn't matter which because there's no hidden stuff that needs tweaking) a meterToFoot function, and apply it as a map. I.e. (pseudocode), ->
feet = {foot, map(roomMeasurements[1], meterToFoot)}
Now, is that perfect? No; even if you as a developer choose to apply type information, another developer can choose to ignore it. But I think that's rarer; more common is when people don't think to supply type information at all, and pass around just (per the example), an array of doubles. You can do that in a statically typed language too, though.
I think the key difference is that people coming from a statically typed, OO language, to a dynamically typed FP language, can either end up creating the same complex data types (which are a nightmare to deal with even with static typing, but doubly so without), or they see all the examples, embrace the simpler data structures...and then don't actually supply the necessary typing information.
The reason, then, that I think static typing -is- good, is because it makes it harder for me to ignore/forget to handle the typing I have provided. That is, I may have a 'metricToImperial' function, that takes in a type tagged array of doubles, and a desired type, and determines and applies the appropriate function. But I can still forget to include a necessary conversion in the resulting case statement ('whoops, I called it with a lb to g conversion over here, and I forgot to implement that one'). It's times like that I really like optional/inferred typing; many places I don't need to check for typing, because it's obvious, both to the developer, and to the compiler if it can infer types...but I can still make mistakes, per that. Of course, that brings me to unit testing...
Looking at the frontend (React) side however, things are different. The JavaScript ecosystem is a mess. From a viewpoint of a React developer, there are lots of libraries which vary widely in quality. react-router is an interesting example here, it had 4 (?) breaking changes so far by replacing the entire api. There's a ton of mental overhead for the normal React developer trying to write a "simple" app.
Ironically, developers start rolling their own stuff. Instead of using a form library which tightly couples your components to your redux state (redux-form), you start writing your own. Instead of coupling your entire views to graphql via apollo, you start doing it differently, your way.
This is where ClojureScript is a game changer. If your app differs just slightly from a (very) vanilla CRUD app and whipping some libraries together doesn't do the trick, you start writing custom stuff. When writing custom stuff, you want a programming language which is a) well thought through (great standard library, immutability, sane concurrency) b) predictable and c) productive. ClojureScript has all three while JS has none.
We (Merantix) are currently developing a medical image viewer in ClojureScript and had prototyped two separate versions: One in JS with React, another one in ClojureScript with reagent and re-frame. Even though it is dependency-heavy (webgl stuff), ClojureScript turned out to be the superior choice: Immutable data structure at its core which ironically perform better than Immutable.js and way higher developer productivity due to less random bugs and a more interactive development (REPL).
Using Clojure on the backend now seemed like an obvious choice: We can reuse and share code from the frontend and more importantly, all our developers are "full-stack" in the sense that everyone can at least understand what's going on "on the other side" (backend / frontend) of the stack as it's literally the same codebase.
The learning curve is significant but the advantages are tremendous. I wholeheartedly recommend learning Clojure even if you're not allowed to use it at your job. It sounds cliché, but it will make you a better programmer for sure.
[1] https://www.youtube.com/watch?v=rI8tNMsozo0 (to understand the meaning of "easy" above)
I recommend everyone to once in a while try to debug Clojure code written by somebody else. Afterwards you will understand that this is a write once, read never language. It is an unmaintainable mess of overly clever recursive subroutines. It has some nice experimental ideas for concurrency. But basically all useful ideas are also available in Java nowadays. I wouldn't waste my time on it.
I don't entirely agree with the "always will be," as Clojure is a very creative language that inspires a wide variety of experimentation, and core.typed is currently being actively worked on (as a PhD dissertation no less by the original author of core.typed), so there is ample room to think the future will offer good static typing abilities for Clojure.
Also, I find your response unnecessarily curt.
If you want proper static typing though, ReasonML might be a good choice. Static, compiles to js and native, super easy to learn, and there’s an experimental Lisp frontend with Clojure-like syntax if you can’t live without paredit.
With Ghostwheel you write your function specs similar to how you'd write a type signature and you get automatic generative testing (including higher order function support) and side effect detection which – when combined with spec instrumentation (+ the upcoming evaluation tracing for the test execution) – can often tell you quite precisely where you screwed up in a much more immediate and granular manner than a simple unit test or mucking about in the REPL could. It really is a quite different experience from plain Clojure.
That being said, I'd love types in addition to this and I'm keeping a keen eye on ReasonML.
I'm unsure of a good way to represent optionality in a dynamic language without a static type system. Or rather, doing so with the tools available -- core.match vs real pattern matching -- seems rather un-ergonomic.
I'm curious about your thoughts of what would be better.
You do have a point!
Having inferred types in addition to this would be even better, but types are no replacement for generative testing or vice versa.
They really complement each other quite nicely, but also have a large overlap in terms of how much they can reduce the need to do manual debugging and enable clean changes/refactorings with minimal effort.
Just a recommendation if you want to avoid the JVM bloat issues, which are real and have existed since the JVM was developed
peace
If you do stick with the JVM, it tends to trade memory for speed, unless you tune it otherwise. For startup speed, start it once, connect a REPL, and keep it running. A started REPL on a JVM instance can be flushed very quickly when you need a blank slate.
But yes, slow starts are annoying if you're starting a JVM frequently. Can you change your workflow so that's not needed? Check out Component, Mount or Integrant.
We're using Integrant already so yes, I can mostly work in a REPL without restarting. But the restarts are more like 15 seconds. I'll have to measure and see what's so slow. My rdev platform is Cursive on a 16G MBP.
And while I'm commenting, yes, we have a reason to run on JVM (a lot of legacy Java code we probably want to interface with later on). So thanks to other commenters for suggesting Node.js and Common Lisp, but not possible. :)
(Not a judgement; this is a legit use case, even though not an environment I like to work in.)
But as long as we're using personal anecdotes, I saw several teams using clojure at Amazon when it was more popular. Less than two years later, all of them that I knew of (I was a clojure user too, interested in its use in the company, so I was keeping track) were at some stage of abandonment or rewrite into more boring languages. If you're lucky enough to keep your team small and ideologically aligned, it might work for your team, but I have yet to see large company make a significant bet on it (as in more than a few one-off teams) and come out ahead.
There are plenty of companies that are switching to Clojure all the time, and feedback is overwhelmingly positive. Here's a recent example https://blog.takipi.com/clojure-at-scale-why-python-just-was...
Again, my personal experience is that my team grew from 3 to around 20 people now all working exclusively with Clojure, and we've never had any problems onboarding, or being ideologically aligned.
Just because it allegedly didn't work out at Amazon doesn't really translate into sweeping assertions you're making about it.
Apple is hiring Clojure people to work on their ML infrastructure. [3]
What's your definition of an inconsequential project?
1: https://www.reuters.com/article/nubank-creditcards/brazilian...
2: https://en.wikipedia.org/wiki/Nubank
3: https://jobs.apple.com/nz/search?job=114084463&openJobId=114...
The current de facto tech stack for building SPAs in ClojureScript seems to be Reagent + Re-frame (or something built on top of them). It's conceptually so close to React + Redux that I have a really hard time imagining a Redux guru who wouldn't be productive with Re-frame in two weeks.
Then of course you can have jQuery developers who absolutely cannot work with React and probably never will, and possibly vice versa (never seen that tested). Back in the day AngularJS was cool but much of the code written with it was horrible, because people wouldn't work through the tutorial which explained how to use it as a tool, not a footgun.