The Next Five Years of ClojureScript
blog.cognitect.com
blog.cognitect.com
"In many ways ClojureScript has and continues to be ahead of the JavaScript mainstream with respect to best practices. Concepts which are only starting to break into the mainstream such as immutability, single atom application state, and agile UI development via robust hot-code reloading are old news to ClojureScript users. "
To my mind, the most compelling thing about Javascript-in-Clojure is working with Om Next. I say this as someone who has recently done some big projects with React.
I think it would be accurate to say that over the last 2 years React has been the Javascript framework that had the biggest impact on how people think about working with Javascript. You look at how much Angular tried to reinvent itself to be a bit more like React.
There are a lot of interesting ideas in React, and I learned a lot while working with it, yet I hate working with it. It is ridiculously verbose. And I hate the way it handles mutations -- it's as if React was trying to be Functional, but then decided to go back and be Object Oriented when it came to mutations.
But I don't mean to go off on a tangent. There are a lot of good ideas in React. Especially GraphQL, which arose as part of the React effort, but which I think will have a long life quite independent of React. React might some day fade away and be forgotten, but GraphQL's critique of the RESTful style is likely to change the way the industry thinks about APIs over the net.
But as I said, React is verbose. I like where Om Next is going -- in terms of immutability and data flow, it has the same basic outlook as React, but Om Next is simpler and it is truer to the Functional paradigm.
If I do any further work with ClojureScript, it will be because of Om Next.
It's likely that I'm just too dumb for modern web programming, but I am waiting for someone to distill the (admittedly brilliant) ideas behind Om into something just as powerful but more usable. Reagent is elegant and simple, but you can see how it falls down in the face of larger apps, re-frame I just find a mess. I'm excited about all of this, but I don't think we're really there yet.
Beyond that, the new reg-effect-fx system isn't particularly mature, individual fx aren't extensible, e.g. being able to extend http-fx to cancel or debounce requests.
I don't _hate_ it, I'm building apps in it. I just think there's a lot of overhead above Reagent that I have yet to see pay off, and the idea that it _forces_ best practises is wrong. It's like an MVC system that lets you put (_encourages_ you to, if you read the docs) controller code anywhere.
If I had to compare it to any other tool, it'd be git. I love git. I use git every day. Do I understand the underlying mental model of git, or the 348 different git commands? Hell no.
Having said that, I'm also pretty sure that Om yields bigger payoffs further down the road as far as I can tell.
My major gripe is with debugging, when I'm writing es6 it's really easy to set a breakpoint and modify a function half way through execution. Or half finish a program and then play around with completions in the REPL to explore a problem. Sometimes I'll be traversing a complex data structure and I'll write an empty loop with just a breakpoint. I can then run the code and work through things in a concrete state and once I've got things working bringing the code back into my codebase.
When I write in any compile-to-js language I always run into the same problem where once I'm in browser I have to go back to vanilla js and any advantage I may have gained is quickly erased by having to deal with transpiled code.
Additionally with libraries like lodash-fp and newer ES features (promises, fat arrows, const and let etc.) the clarity in vanilla JS code is approaching a reasonable level.
Its just another hard to debug compiles to js language.
I get you can share some code between server and client... but we've used it in production and now we're getting rid of it; there just wasn't a compelling reason to keep it over es6.
Even hitting submit twice on an standard ajax powered form is likely to cause mild concurrency-related bugs on many popular websites.
>> there just wasn't a compelling reason to keep it over es6 <<
How about these reasons:
- much simpler language - core.async channels/ no callback hell, easier to read async code - persistent data structures - great sequence abstraction - live code reloading - vibrant community - improved tooling - macros - transit
Given that it all compiles down to JS before execution, I'm not sure I agree. With Java, you're going to bytecode operations which (imo) allows for a simpler language than compiled to.
>> - core.async channels/ no callback hell, easier to read async code
I agree, callback hell sucks. I'm not sure that core.async is a huge win over promises, but this is an area that JS is quite horrible.
>> - persistent data structures
>> - great sequence abstraction
I'm not sure I see the benefits of clj's sequence abstraction over js. You get destructuring and sequence comprehension in es6, although clj's recur/callstack fix is quite nice.
>> - live code reloading - vibrant community - improved tooling -
I'm pretty sure all of these are better in JS than CLJs.
>> macros - transit
Macros are the old LISP fallback, but seem very counter to any argument of 'simplicity,' I very rarely find need for macros in the front and Clojure's design philosophy generally favors not using them (Data > Functions > Macros)
I do like CLJ and CLJS is interesting, but the only reason I really reach for CLJS is if I am feeling like writing something in om.next.
Most likely, they meant "simple" more in the syntactical sense than in the sense of language abstraction level.
> Macros are the old LISP fallback, but seem very counter to any argument of 'simplicity'
Macros is just transformation over a data structure, albeit a data structure that is interpreted as code later on. It shouldn't add more complexity than other forms of data transformation.
> I very rarely find need for macros in the front…
Neither do I, but I'm glad they exist. Core.async is built with macros, for example. They are part of what makes it possible to have core.async as a library rather than having to build it into the language.
Outside of the Clojure world this would be a reasonable interpretation. However, if you watch 'Simple made easy' by Rich Hickey - that was the definition of simple I was going with (as opposed to easy.)
>> Macros is just transformation over a data structure, albeit a data structure that is interpreted as code later on. It shouldn't add more complexity than other forms of data transformation.
Because they specifically inject before the eval section they _do_ add a lot of complexity. Writing complex macros is a lot of guesswork.
>> Neither do I, but I'm glad they exist. Core.async is built with macros, for example. They are part of what makes it possible to have core.async as a library rather than having to build it into the language.
I'm not sure what the benefit of this vs 'language features' is at this point.
Even if a macro is buggy, if the particular instances (i.e. macro calls) don't step on the bug, then we are okay. The expansion is done, and it is good.
Macros are deterministic and susceptible to regression testing with simple "X translates to Y" assertions.
We don't have to be overly concerned with the time and space performance of macros, either. The code in a macro expander can be structured for clarity and remain that way.
(Even in situations in which running Lisp apps are patched, the macro expansion doesn't have to be done in the application image. The translation unit can be compiled using a separate development image, and loaded as compiled files in the running app.)
If knowledge of how to use macros was better, I think some of the macro FUD would go away. If you're not using macros then you might as well not use a LISP. I'd agree though that using macros improperly leads to all kinds of problems.
Data structures in clojure are unimaginably better. In js you don't have anything like this. For example you can't tell how operations in js will perform since not even their complexities are documented.
Please elaborate about the better community. AFAIK js libraries have the half-life of months and you can't rely on them not being obsolete after you finish a project.
Macros are a tool which you don't have to use. Everything in the clojure world is optional. But when you need it you have something powerful. Try and emulate macros in js (like AOP in java) and you are in for a rude awakening.
...but really, it boils down to the fact that the people who were writing clojurescript in house wrote terrible spaghetti code that didn't work.
Without a tangible justification for re-writing (again) in clojurescript, the decision was made to rewrite it in es6 and throw all of the clojurescript away.
I think the lesson here is:
Having an excellent language (Clojure) can't save you from writing bad code.
For some reason writing clojurescript resulted in code that was a far lower quality than the clojure code from the same developers. /shrug
I can't explain why that is, but for us, it boiled down to: If you can't play nicely, you can't have nice toys.
Quality of the end product is more important than the tools used to build it.
Using Figwheel, this is just not true.
I do see ES6 making life hard for the compile to JS languages, though.
Not that JS doesn't have debugging pains either, but really the debugging story is the worst part of clojurescript.
Another thing to consider is how many of your bugs are because of an ill specified object, or because i held null instead of integer.
Also, I think the idea is to write specs for stuff that happens on the edges of the app, such as data received on the wire. If you have that covered, it probably would catch most problems you might encounter, such as receiving a null in a place where you expected a positive integer or such. Since it also destructures the data for you, specs for stuff received on the wire would save you the time you'd otherwise spend taking it apart manually, while simultaneously making sure it's not malformed.
Although I'm not that thoroughly read on spec, so I'm not sure I'm taking everything into consideration.
- hiccup syntax / sexps are trees analogous to the HTML you're constantly generating, and first-class supported data structures supported by the language which need no pre-compilation/transformation step, unlike JSX
- syntax is a good 20-30% less verbose than JS (yes, even ES6), especially when the core lib is taken into account (things like partition, zipmap, threading macros)
- one extremely battle-tested dependency resolution story, not named NPM - it just works
- incredible development tooling innovation, best of class for frontend web development
- the sequence abstraction and dozens of functions that operate on it, rather than Object.assign, Array.map, etc
- best-of-breed dead code elimination via Google Closure Compiler, meaning much of third party libraries and development-specific code ends up weighing less / nothing in production builds
- sets and set operations built in, along with tree walking, diffing and string utility libraries, included with the distribution
- no more defensive copying, thanks to immutability everywhere
This alone is worth all of it.
It has many advantages over those tools imo - but its dependency resolution model isn't one of them.
JSX works fine, looks like HTML, and Cljs needs precompilation anyways. Sexps being a pure representation of HTML trees has absolutely zero impact on productivity.
I use Closure Compiler, hot reloading and native Sets with regular ES6. I agree that immutability is a must and JS immutable libraries are just not ergonomic. If at least JS had operator overloading...
I love Clojure, but it just doesn't click for me in the frontend.
Clojurescript for skeptics: https://youtu.be/gsffg5xxFQI
That sucks because cljs is great but it's probably the biggest incentive for sticking with JS.
Clojure is growing fast, and a complaint I'm hearing consistently lately is that there are not enough clj/s devs around.
In Clojurescript, it's my opinion the Closure compiler is of dubious value. It adds yet another level of indirection between the source and the target. And isn't it the dead code elimination that makes run time macros impossible in CLJS? They are possible in Clojure proper.
Even so, embedding a CLJS compiler in your app (and hence runtime macros) is possible.
I agree, but with code splitting this could be done with 0 overhead for most use cases.