5 ms·
I stopped using clojurescript around 2015, primarily because I was tired of searching for/implementing clojure-idiomatic wrappers around the many javascript lib
by bikeshaving 7y ago
I stopped using clojurescript around 2015, primarily because I was tired of searching for/implementing clojure-idiomatic wrappers around the many javascript libraries out there. Put another way, I left clojure on good terms, and often missed its core functions, immutable data structures and lisp syntax when writing front-end applications. However, since then, javascript has changed a lot, specifically in terms of execution-suspending operators like async/await/yield/yield*. Meanwhile, the core clojurescript team has almost completely ignored these developments, preferring the immutable seq abstraction over iterators, and the core.async channel abstraction over promises/async iterators.
For this reason and this reason alone, I’ve personally stopped considering clojurescript as a viable technology or recommending it to others. I have no idea how comtemporary clojurescript developers interoperate with javascript libraries and built-ins which are now overwhelmingly promise-based when async. I am also incredibly skeptical of the core.async channel abstraction as an alternative to promises/async iterators insofar as I think it is strictly inferior to both promises and async iterators because:
1. There’s no standard way to communicate error conditions.
2. There’s no logical separation between data producers and data consumers, and any code with access to a channel can take or put values.
3. There’s no way to communicate nil values insofar as core.async chose to use nil to represent end of iteration.
I do not understand the decision by the core clojure team not to support promises or iterators, because one of clojure’s main selling points was pragmatic interopability with host language features. Nevertheless, I infer it must be an intentional decision not to support them, insofar as it’s so easy to support new language features in clojure with macros. Maybe it comes from a desire to adhere to “functional programming principles,” but I’ve found async/await, promises, and iterators/generators to be incredibly useful, so much so that I am unwilling to give it up for clojure’s immutability or nicer syntax.
- wellpast 7y agoAt first glance the cljs community doesn't seem short on libs that give await/async-like abstractions and from what I recall JS interop in CLJS was smooth enough to operate directly against Promise objects. In Rich Hickey's famous / often-referenced "Simple Made Easy" talk he lists what he considers simple vs complex toolkits. [1] There's a clear preference for declarative constructs vs imperative constructs. Async/await is attempting to take async behavior and make it "appear" synchronous. I suspect Hickey would slot this into the complexity category. At least I tend to agree. I think channels are both explicit about what is going on and also maximally decoupled/composable. It seems that your preference leans the other way. I just point this^ out b/c it is possibly or likely an explicit choice and not a miss by the Clojurescript team (Nolan?). I personally find the freedom given by Clojure/script ultimately simple and powerful. Curious for others' thoughts on this. [1] https://www.google.com/search?q=%22simplicity+toolkit%22+hickey&source=lnms&tbm=isch&sa=X&ved=0ahUKEwiBj9S1lKnlAhVJLKwKHXBSDgkQ_AUIEigB&biw=1467&bih=750 https://www.google.com/search?q=%22simplicity+toolkit%22+hic...
- ithkuil 7y agoI'm confused. Doesn't core.async do the same? The go macro applies a transformation on the body that is analogous to what happens with async/await. The difference is the async/await contract is built on top of promises which define a single future value or an error, while core.async uses a channel which can only yield non-nil values (no place for errors). Yes, you can do more with channels. But you can also use them to implement simple "blocking" calls that return one single value (This is used quite a lot in Go, where doing basic blocking IO on a file involves underlying channels between a facade layer and IO scheduler thread). Presenting an illusion of sequential code is precisely what core.async does. It's magic and hides complexity (which can leak back and bite you)
- wellpast 7y agoYou're not confused. I think you're right that both cljs/go and js/async blocks are giving a 'procedural way' to basically create an asynchronous processing/state machine. It would be interesting to see a fully-baked out analysis of async/await vs go and Promises vs channels. I do think Clojure still has the win on simplicity if for the main reason that core.async is just a library [2] and Javascript implements async/await via syntax with the actual implementation at a subterranean level. Clojure's version (core.async) can be macroexpanded/unwound -- i.e., the 'magic' you speak of is not magic at all and can be examined and implemented directly w/in ClojureScript itself. From the channels vs Promise perspective I think there is more to argue. Channels are quite a simple concept when weighed against the Promise spec [3]; of course this is arguable and arguments welcome. Also worht considering David Nolen's takes, one of them here: [4] [1] https://clojure.github.io/core.async/ https://clojure.github.io/core.async/ [2] https://clojure.org/news/2013/06/28/clojure-clore-async-channels https://clojure.org/news/2013/06/28/clojure-clore-async-chan... [3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe... [4] https://swannodette.github.io/2013/08/23/make-no-promises https://swannodette.github.io/2013/08/23/make-no-promises Edits: typos/cosmetic.
- ithkuil 7y ago
- i_s 7y agoAt my job, I work on a ~40 KLOC Clojurescript re-frame app. It has been working very well for us, and none of the things you bring up have been an issue. In re-frame, programming with channels is pretty rare. We have it maybe 2 places in the whole app, and one of those places is an internal load-testing type screen. If core.async were gone tomorrow, I would rewrite maybe 30 lines of code, and not really care - it just isn't important at all. I think in Clojure in general, rolling your own stuff is more common, so even if I agreed that interop was difficult, it just isn't that common. A lot of the time, integrating complicated libraries is not worth it if you can just make your own very easily.
- bikeshaving 7y agoWhat is the async story for clojure if it isn’t core async? How do you make server calls? If you’re still using callbacks, you’re really missing out on the convenience that async/await provides. Also, are you using core.spec or transducers regularly? Do you find them understandable? How has the clojure community responded to react hooks? There’s so much that I’m curious about regarding present-day large clojure codebases, sorry.
- capableweb 7y agoIn re-frame you would dispatch a event to tell your application you need some data to be fetched. Once the data has been fetched (or received an error), it dispatches another event. So basically with re-frame applications, it's all events. Also, I think the whole "callbacks vs async/await (promises)" is only relevant in JS, where callback-hell is easier to end up with. Mostly because clojure as a language (lisps really [I think?]) prefers very small, composable functions. So by just following that, you avoid callback-hell. I'm also working on a clojurescript codebase in my daily work. Using clojure.spec to specify behaviour and simulate data for my reagent components. Transducers I have not have the need to use yet. Regarding React Hooks, when using reagent together with re-frame, you basically end up with all components being state-less, and having the react hooks would just couple functionality to one component, so it's not really needed.
- i_s 7y ago
- adz5a 7y agoHello, this is a really interesting comment although I disagree with some of its points. I recently had my first full time Clj/Cljs job and it made me realise some points: - although figwheel is an awesome build tool, shadow-cljs with its near seamless npm integration is imo the most powerful Cljs build tool atm [1]. - cljs-bean[2], recently released by Mike Fikes is a really cool tool: it makes working with POJO really easy without too much performance penalty - the new react hook API is imo a game changer for CLJS frontend frameworks: the decision to overload native function for componenents instead of using ES6 classes makes working with react componenent much more easier, imo this new API suits CLJS perfectly: hooks are just js functions that are called in the same order at each render pass. - clojure.spec and clojure.spec.gen in a REPL are really powerful tools for frontend development that are unavailable in the wide JS world. All in all I am actually much more optimist than a few months back about the future of CLJS frontend dev, those are still relatively new tools, clojure.spec is still a work in progress (although I would recommend to take a look at the metosin spec-tools which are really nice). Edit: formatting [1] https://code.thheller.com/blog/shadow-cljs/2018/06/15/why-not-webpack.html https://code.thheller.com/blog/shadow-cljs/2018/06/15/why-no... [2] https://github.com/mfikes/cljs-bean https://github.com/mfikes/cljs-bean
- kendru 7y agoI was not aware of cljs-bean, but it definitely looks like it's worth checking out!
- ambulancechaser 7y agoHow can you use hooks right now? The standard is reagent which compiles down to classes which don't work with hooks. I think reagent has given a functional react and hooks style programming for years but now is a little behind the curve.
- capableweb 7y agoYou would do what Reagent does and use JS interop yourself and call React directly. Plenty of people in Clojure-land implement things themselves if the thing to implement seems simple enough. Probably more true for Clojure developers than let's say JS/Ruby developers.
- yogthos 7y agoMy team has been working with ClojureScript for close to 5 years now, and we haven't had any of these problems. Promises work just fine, you don't need any additional support for them. For example, this works just fine: (defn parse-image-links [pages] (-> (map #(js/Promise. (parse-page %)) pages) (js/Promise.all) (.then #(->> % (map :Images) (apply concat) (map parse-image-url) set save-links)) (.catch js/console.error))) https://gist.github.com/yogthos/d9d2324016f62d151c9843bdac3c0f23 https://gist.github.com/yogthos/d9d2324016f62d151c9843bdac3c... Here's another example working with async Ajax calls from re-frame https://github.com/ClojureTO/JS-Workshop/blob/re-frame/src/reddit_viewer/events.cljs https://github.com/ClojureTO/JS-Workshop/blob/re-frame/src/r... Furthermore, we find that we rarely need things from Js ecosystem. Here's [1] a talk I recently copresented on an app my team built. I has around 50kloc ClojureScript, and we barely had to use anything in Js ecosystem for it. https://www.youtube.com/watch?v=IekPZpfbdaI https://www.youtube.com/watch?v=IekPZpfbdaI
- Scarbutt 7y ago(defn parse-image-links [pages] (-> (map #(js/Promise. (parse-page %)) pages) (js/Promise.all) (.then #(->> % (map :Images) (apply concat) (map parse-image-url) set save-links)) (.catch js/console.error))) And you have gone full circle, bact to writing Javascript but with parentheses.
- altanil 7y agoYes. At its worst it's just Javascript (with parentheses), especially when dealing with libraries and promises. In other cases you can benefit from all the features of ClojureScript. That's pretty magical in my experience.
- capableweb 7y agoYou could do more! Use Array.map and Function.apply via interop and you seem to have it all in JS
- yogthos 7y agoThe thing is that interop typically lives at the edges of the application where IO happens. I'm typically using a Js library to do Ajax, paint stuff on the screen, and so on. All the core logic of the application is written in pure ClojureScript. And that's where all the interesting things happen.
- didibus 7y agoI think there is a tendency to think... My current language would really benefit from having X,Y and Z. And then going to look for an alternate language that has X,Y and Z. But only your current language might benefit from X,Y and Z. It is not true that if you change the environment say to ClojureScript, that you will still feel the need for X,Y and Z. I think this is what is happening here. Now, obviously, until you unlearn old environment and re-learn new environment, you will be confused, and probably still feel the need for X,Y and Z... And why would you continue to learn this new language anyways if you were initially motivated by X,Y and Z only, and not learning a different style/paradigm? I think this is a little bit what's happening with the JS vs CLJS debate. The styles and needs of ClojureScript are not exactly the same as JS. For example, the Promesa library used to have an async/await, but removed it with: "they don't demonstrate to be useful in comparison to the complexity that they introduce". Now, some of that complexity was in their implementation, but I think its true that most of the other constructs in Promesa kind of solve all the common use cases for async/await as well. There's a coroutine library here: https://github.com/leonoel/cloroutine https://github.com/leonoel/cloroutine which you can use to build async/await and generators: https://github.com/leonoel/cloroutine/blob/master/doc/01-generators.md https://github.com/leonoel/cloroutine/blob/master/doc/01-gen... and https://github.com/leonoel/cloroutine/blob/master/doc/02-async-await.md https://github.com/leonoel/cloroutine/blob/master/doc/02-asy... So it can be done, if you want, and I think you should play with it and see if the style bode well within the context of ClojureScript, but the lack of people trying it out to me indicate that its not something they feel as strongly being needed. Keep in mind, generators and async/await are very imperative in their nature, and ClojureScript is primarily a functional language.
- zelly 7y agoA few years ago when I tried it, I got frustrated with it very quickly when I found out macros are not allowed in ClojureScript. I think you should either write JavaScript directly or compile to WebAssembly from some better language. Transpilation has always seemed to end in failure.
- ithkuil 7y agoMacros are allowed in clojurescript. It's just that they cannot be defined in the same compilation unit where they are used. Yes it's a pain, but not nearly as your phrasing makes it sound like. That said, I'd also love a native webassembly target.
- iLemming 7y ago> macros are not allowed in ClojureScript Macros ARE allowed in Clojurescript, they just need to be defined in other than a Clojurescript file, because of the way how macros work. And you still can do pretty cool stuff with macros. Also if you really need to, you can actually define a macro in Clojurescript that would use `eval`, but nobody does that, because it's rarely needed. https://gist.github.com/alandipert/4669472 https://gist.github.com/alandipert/4669472 > Transpilation has always seemed to end in failure There will always be a possibility for languages of higher-level of abstraction, e.g: Assembly -> Fortran Assembly -> C -> Python JVM -> Java JVM -> Scala etc. There will always be languages that transpile, compile into Javascript. It is especially becomes more relevant with the grows of JS, which is no longer a "small scripting language", ECMAscript spec is over 800 pages long today and they keep adding stuff. Java and JVM kind of become "assembly" languages for their respective platforms. There are many benefits from using the same language on both back-end and front-end. And besides Nodejs, there are a few other options, including Clojure. JS devs even been transpiling one version of ECMAscript to another for many years. If doing that was a "failure", how can you explain so many small and big companies have adopted that model?
- dominotw 7y ago> preferring the immutable seq abstraction over iterators, and the core.async channel abstraction over promises/async iterators. do you know why javascript async and co are superior to this?
- bikeshaving 7y agoI’m not sure if they’re superior. One thing that iterators can do which clojure lazy-seqs can’t is that with iterators, we have the concept of abrupt termination or return. In javascript, this is analogous to calling break within an iteration block, or calling the return method directly on the iterator. There is no equivalent concept with clojure seqs, meaning you can’t do things which tie down resources, like creating a database cursor and yielding the results. With a generator, you just make sure that you close the cursor by wrapping your yield operators in a `try/finally`. There is no equivalent for a recursive lazy-seq function, even though clojure does have `try/catch/finally` expressions. As far as why promises are superior to core.async you can refer to the list in my comment above.