Fork me on GitHub

Greetings! I read Joy of Clojure, 2nd ed. (JoC) cover-to-cover back in 2014 when it came out — helped me a lot in my clojure journey. In 2017, various happenings pulled me away from clojure and back to node.js, python, etc. As of today, my team of 6 (myself being the only one w/ prior clojure experience) needs to ramp up fast on the core clojure lang (broader ecosystem not so important, for now). Any recommendations re: books, etc.? Is there a book/site considered the “successor” to JoC? I’m mainly thinking of my teammates, i.e. how much I appreciated JoC helping bootstrap me into clojure and functional programming. I can personally sort out what’s changed/added since 2017, but I’d like to give them some good resources for starting from scratch; they’re all accomplished in JS and some other langs, but none of them has touched a Lisp.

Alex Miller (Clojure team)01:04:00

Programming Clojure 3rd ed is probably the most up to date (1.9 era)


:thumbsup: thanks!


aside: I still fondly remember Strange Loop 2011 where I completely-accidentally ended up going to dinner at Pappy’s Smokehouse on Olive (in St. Louis City) with Dr. Sussman and a random group of attendees — he came out of some side-room, saw a group of us standing around, and asked us if we’re hungry and whether we would like to have dinner with him.

parrot 4

another newbie question: if i wanted to throw an exception AND return a custom json response, how would i do that?


ex-info is usually used to pack in more info along with the exception. You can retrieve the info back with ex-data.


Yup, what he said. ex-info is designed for "information-bearing exceptions" and this sounds like a good fit.


(and the latest version of Clojure adds ex-message as a standardized way to get the message from any exception)


If ex-info is for information-bearing exceptions, the answer for non-information-bearing exceptions are classic java exceptions?


(of which I don't know any)


What I mean, is that when throwing my own errors, are there situations where I should use java exceptions (like this one for example, or should I always use ex-info for my own exceptions?


It depends. You could, e.g. thrown an IllegalArgumentException when you receive an invalid input data although it's not very common. However, if you feel like you want to create your own exception class then you should use ex-info instead, imho.


@U7S5E44DB if an existing Java exception matches the problem you want to report -- and you don't need to convey any additional information -- them it's reasonable to throw the Java exception.


Just remember that exceptions are not intended for "expected error conditions" or flow of control -- they're for "I can't handle this!" situations.

👍 20
💯 4

awesome advice everyone! i was looking at ex-info yesterday and it does throw the exception with the info i provided however i would like the exception thrown and also a response like

   "status": "4xx",
   "message": "My Custom Message Here"
but i am just seeing an HTML response of the exception being returned


does that make sense? if not i can add more details tonight!




ex-data is probs what i need as mentioned above!


i will try that and report back!


I think ex-data is what you need. Whatever you return from the HTTP handler will be sent back to the browser.

👍 4

perfff!!!! ill give it a try tonight and report back!


again the cloj community helping me out as always! so excited

💯 4

ok so after looking at this more i THINK what i really want is clojure's spec functionality to do validation on incoming client requests


@U0D5YAGUX That's one of the ways we use Spec -- we write specs for our API's input parameters and then we s/conform the params and check if s/invalid? -- if it is, we respond with an s/explain-data mapped to client error messages, else we used the conformed parameter data.


We opened sourced some of our low-level spec utilities for dealing with API parameters


@seancorfield do you have any examples of how to use web-specs?


(s/conform ::some-spec some-value)


Then check s/invalid? on the result. Just like you'd use any other spec.


Not sure what to say beyond that. They're just specs.


You can also use s/valid? with the spec directly, but that doesn't give you any conforming of input data.


Hi, I am getting namespace not available in some template code where all I did was that I renamed the existing files and namespaces to something new. Now, I can just restart from scratch and be more careful to avoid this situation, but I figure there should be some way to debug this, right? So what can I check to fix such an error? I tried to read docs but that just explains at length what is happening when everything is working, but there is not a word anywhere on what to do when anything is not as is intended


$ lein new lib foo Failed to resolve version for lib:lein-template:jar:RELEASE: Could not find metadata lib:lein-template/maven-metadata.xml in local (/Users/nick/.m2/repository) Failed to resolve version for lib:lein-template:jar:RELEASE: Could not find metadata lib:lein-template/maven-metadata.xml in local (/Users/nick/.m2/repository) This could be due to a typo in :dependencies, file system permissions, or network issues. If you are behind a proxy, try setting the 'http_proxy' environment variable. Could not find template lib on the classpath. $ lein new app foo Generating a project called foo based on the 'app' template. Does anyone know why lib lein template is the only one that's falling?


Isn't lib the default template? lein new foo


I suspect they're using different templates between lib (explicit) and default/implicit.


That's right. Thank you!

Jim Newton10:04:04

I'm looking at the code in core.clj for clojure-1.10.0. I notice the following function definition:

(defn decimal?
  "Returns true if n is a BigDecimal"
  {:added "1.0"
   :static true}
  [n] (instance? BigDecimal n))
Is this correct? is BigDecimal the only type of decimal number which clojure supports?

Jim Newton10:04:45

For example, this causes 1.0 not to be rational? (rational 1.0) returns false, while (rational 1) and (rational 1/3) both return true.

Jim Newton10:04:30

more playing with types, It is a surprise that (symbol? :x) returns false. In clojure, keywords are not considered to be symbols?


Keywords and symbols are disjoint sets of values in Clojure.


The literal 1.0 in source code is a Java Double type, a 64-bit IEEE 754 floating point number. Thus it can only exactly represent a "few" fractions exactly.


decimal? is just Clojure's predicate function to determine if a number is BigDecimal or not. Java integer and Clojure's Ratio type are also exact numeric representations with no loss of precision. Not sure if that answers any of your questions, though.

Jim Newton11:04:34

I have a problem I don't know how to approach in Clojure. I have a dynamic variable, in my case it's named *rte-known* . It has a default value which is a map (hashmap). The intent is that an application may rebind this variable, and extend it using a call to assoc to establish more known values for a certain dynamic extent. That works find and great. However, during the ensuing computations, I'd like to memoize some expensive computation results, and have them go away when the user's binding of *rte-known* goes away. If I attempt to use a global mutable object, then after the user's dynamic binding finishes its dynamic extent, then my globally memoized data is likely to be invalid. I think this means that I want to associate a mutable value with a dynamic binding. Is this possible? The only way I know of doing this, as to provide a macro which the caller is expected to use to rebind *rte-known*. And that macro would bind two values, *rte-known* and a secret mutable object. Is that the correct way? Or is there a better way?


(defn expensive-computation* [_ a b c] ...) (def expensive-computation (memoize (fn [a b c] (expensive-computation* *rte-known* a b c))) perhaps?


If I understand your problem correctly then you can bind the dynamic variable to an e.g. atom to hold the computation state

Jim Newton12:04:15

@UTQEPUEH4 looking at the implementation of memoize, I doubt your suggestion will work. It seems memoize creates a closure, which closes over an atom on which it uses as a memoization table for that function. this memoization table remains until the closure is garbage collected, if ever. It is not recreated and released when the user binds a particular named variable.

Jim Newton12:04:24

in stead, according to my interpretation of the function, if the value of goes out of dynamic scope, then these values will stay in VM, memoized on that secret atom, until the JVM session ends.

Jim Newton12:04:33

@UKQS17P3L, I think that means I'd implement a macro for the user to use to rebind the value. the user would not call assoc directly, but would call my with-known-values macro.

Jim Newton12:04:08

it's a good idea, saving me the effort of implementing a memoization protocol.


Cool glad I helped

Jim Newton13:04:53

Glad I asked this question, as I didn't know about memoize, and its a function I've written from scratch multiple times in other lisps. The documentation says it is used to memoize the return values of a pure function. However, the introduction of a dynamic value violates this assumption. i.e., the return value now depends on both the arguments and the dynamic extent.


Of course the memoization store remains; but if your dynamic var has changed since something was cached there, those cached results will not be used for the next calls


I've got the code wrong though, of course. Should be something like (def expensive-computation* (memoize (fn [_ a b c] ....))) (defn expensive-computation [a b c] (expensive-computation *your-dynamic-var* a b c))

Michael J Dorian12:04:16

I'm not sure about dynamic vars, but if your top level map was an atom, you could set up a watcher and reset! another atom with the cached data as soon as your first atom is blank

Gabriel Saliev12:04:14

Hello, can somebody explain why the function conj applied to a list conjoins at the beginning but when I use it on vector it appends it at the end, thanks!

Michael J Dorian12:04:53

It's because of the differing implementation details of those collections. You can add things onto either the front or back of either datatype, but conj uses the faster operation.

👍 8
Gabriel Saliev12:04:25

thank you very much!

Endre Bakken Stovner12:04:07

Can I implement a new def without a macro? I think I need a macro to write stuff like (defrule burrows-wheeler {}) where burrows-wheeler is undefined.

Alex Miller (Clojure team)12:04:41

no, you need a macro

👍 4
Alex Miller (Clojure team)12:04:48

in general, defwhatever is usually expect to be something that defs a var and is typically a macro that emits some other def form (def, defn, etc)

Endre Bakken Stovner12:04:55

Thanks. First time I've had use for (writing) a macro. Finally XD

Alex Miller (Clojure team)12:04:21

it's probably worth looking at the source for some other def macros

👍 8
Alex Miller (Clojure team)12:04:37

(defmacro defrule
  [name val]
  `(def ~name ~val))

Alex Miller (Clojure team)12:04:49

is a good shell to start from

💯 4

Question about running tests (specifically with lein test in my case) - I am structuring some tests like so

(deftest addition
  (is (= 4 (+ 2 2)))
  (is (= 7 (+ 3 4))))

(deftest subtraction
  (is (= 1 (- 4 3)))
  (is (= 3 (- 7 4))))

(deftest arithmetic
It appears however that the default test selector causes each deftest to run, causing the same tests to be run multiple times. Is the conventional approach to annotate arithmetic with some a metadata key like ^:regression, and in my project.clj configure lein to only run tests tagged ^:regression? I feel like I also might not be understanding how clojure.test works, and am duplicating my tests on accident. Am I composing my tests incorrectly? Do I need the metadata / lein configuration? Thank you


addition and subtraction can just be functions


calling a function with is calls inside inside a test just works


calling a deftest inside a deftest seems like it would never be a good idea


That example was straight from the DEFINING TESTS section of the docs


Maybe I misunderstood what they were saying. Functions work fine.


right - I think functions are more likely to allow the behavior you want, I can't think of a usecase where you'd both define a test (which means clojure.test would run it when testing an ns) and also call it from another test


unless you were using binding to set dynamic vars, or calling tests from another ns


That is odd that that example is in the docs.


So how would you have nested tests, if deftest isn't the way to go?


From the docs:


(deftest arithmetic
> The names of the nested tests will be joined in a list, like > "(arithmetic addition)", in failure reports. You can use nested > tests to set up a context shared by several tests.


I don't understand the premise, I have never needed a test that invoked other tests, I use the testing macro for that


(ins)user=> (source clojure.test/testing)
(defmacro testing
  "Adds a new string to the list of testing contexts.  May be nested,
  but must occur inside a test function (deftest)."
  {:added "1.1"}
  [string & body]
  `(binding [*testing-contexts* (conj *testing-contexts* ~string)]


OK, that works. Thanks.


I dunno how I missed that.


Really I just want to group similar tests together. I may have confused myself with the examples in the docs.


From the deftest docs: > > Usage: (deftest name & body) > Defines a test function with no arguments. Test functions may call > other tests, so tests may be composed. If you compose tests, you > should also define a function named test-ns-hook; run-tests will > call test-ns-hook instead of testing all vars. Suggests you create your own function test-ns-hook to call just the tests that should be called, to avoid double running.


ah that was the piece I had missed


It was well hidden 🙂


I ended up doing something akin to

(deftest related-features
  (testing "Feature A"
  (testing "Feature B"
where (test-feature-X) are just functions using the is macro. Works fine

💯 4

Thanks so much @teodorlu, that's really helpful!

👍 4

Hi, is it acceptable to share a let binding with two defns?

(let [x (atom 10)]
  (defn f [] (inc @x))
  (defn g [] (dec @x)))


I mean, these are just examples. Actually I want to share a websocket connection with two functions.


But I don't want to leak the connection to other methods.


the idiomatic thing is to make x a def - normal clojure code doesn't do much data hiding


if f and g should be so tightly coupled, make them methods on a record or type

👍 4

making a websocket connection a global (a def) is very suspect


every deftest is run


so if you want to decompose things like that the thing to do is to make addition and subtraction a regular defn

👍 4

I am a little confused about swap!. The docstring states: "Atomically swaps the value of atom to be (apply f current-value-of-atom args) " However, (swap! (atom 0) max []) throws, while (apply max @(atom 0) []) => 0 . Is the simplest way to get around this just to use reset! with the current state of my atom (`(let [a (atom 0)] (reset! a (apply max (conj [] @a))))`)?


In (swap! (atom 0) max []) args is not [] , but [[]]


because args is the list of arguments, and your list of arguments contains one thing (an empty vector)


ah right, thank you


I want to use (apply swap! ...)


probably not, you want to use swap! your-atom max (if you really want to call max with just one argument, that is the value of your atom)


I find it strange though; can you describe what you are trying to do?


if you have a coll and want to swap! to assign to the max of current value / values in coll, (apply swap! a max coll) makes sense


(cmd)(user=> (def alternates (repeatedly 10 rand))
(ins)user=> alternates
(0.9628561654175068 0.11902505003214825 0.3835041588382112 0.5507751502080879 0.18499959134452892 0.013132287289970068 0.00873908073221663 0.890933501821531 0.8008623048866589 0.9502918850062402)
(ins)user=> (def a (atom 0))
(ins)user=> (apply swap! a max alternates)
(ins)user=> @a


yup, that's precisely what I'm doing!


what are (cmd) and (ins) in this prompt?


kind of looks like a pasting artifact from emacs?


my readline config asks for the vi mode of my prompt to be visible


it saves me typing extra esc / i to be sure of my mode

👍 4

Is there any way to tell leiningen to install a package from the command line similar to how npm works (ie npm install react => lein install cljfmt)? It kinda sucks to have to manually edit project.cjs every time you want to add a dep


this is fundamentally opposed to the premise of lein (and other m2 based dep managers like maven, gradle, and deps.edn) the goal is that every dep is project local and versioned, so you never break a project by changing a dep for another project, you can't do this if you are using global installs

☝️ 8

that said, you can create a profile in ~/.lein/profiles.clj that automatically includes the deps you want when experimenting and not committed to a specific project


that way you can opt-in via lein's with-profile to add the deps to a repl, without polluting all projects


In the case of cljfmt, the readme describes tools.deps settings that can be wrapped in an alias/script and used independently of lein


% cat ~/bin/cljfmt
clojure -Sdeps '{:deps {cljfmt {:mvn/version "0.6.4"}}}' \
  -m cljfmt.main $*
% cljfmt check core.clj


and tools.deps / deps.edn has its own user config for opting into a set of deps for your repl or task via a profile (using the terse -A flag instead of lein's verbose -with-profile)


A util that added the latest dep version as a line to your deps.edn or project.clj might be a nice convenience


% cat ~/.clojure/deps.edn
 {:cljfmt {:extra-deps {cljfmt {:mvn/version "0.6.4"}}
           :main-opts ["-m" "cljfmt.main"]}}}
% alias cljfmt='clj -Acljfmt'
% cljfmt check run.clj


So there are a few issues here that have been brought up. However, my original question was not relating to those (although thank you for the information). The two issues are: 1) Global vs Local installs. Sorry I was not clear, but I was explicitly referring to Local dependencies and the convenience of installing them via command line. In other words, I was hoping there was a simple lein task/command to install local dependencies in your project.clj via command line. Ala npm npm install react This automatically updates the project-local package.json with the latest react package dependency. Similarly I was hoping something like: lein install clfmt Automatically updates the :dependency value in project.clj with the latest version of cljfmt However, there doesn't seem to be anything like this (at least from what I've googled). Which is a bummer


To the latter point, I have seen lein ancient that can find deps in your project that can be updated. As to adding deps from the command line... it just doesn't seem like something lein is trying to be. But lein is also extensible; maybe someone has/will do it in a plugin. :man-shrugging:

👍 4

this tool seems to do what you want for deps.edn


and you can tell lein to use deps.edn, so that should be what you want? add find-deps to your personal deps.edn under the alias find-deps then follow the instructions on the readme


nice. Thank you, I will take a look!

Michaël Salihi08:04:29

@U050MP39D Which editor do you use ? 2/3 months ago I was looking for the exact same answer and today I actually use find-deps but also clj-refactor in Emacs / CIDER which allows me to add the libraries via the cljr-add-project-dependency command like this:

Michaël Salihi08:04:57

By the way, it works for Leiningen and project.clj too

Michaël Salihi08:04:35

Direct visualization in GIF


if I were writing enough clojure to warrant it I'd be using cursive. but atm I just use command line repl + occasional vim, so find-deps via clj -A:find-deps -F:save works for me


I'm trying to write a GUI in Clojure, and it seems like I need to pick between JavaFX and Electron bindings. What are the pros or cons of each? (I don't know either framework, so I'm going to have to learn one of them)


Hi! I wrote cljfx, and I’ve been trying to make somewhat unbiased comparison between that and electron after I wrote Year of Clojure on the Desktop ( and received a lot of criticism from the js devs 🙂 In short: - both cljfx and electron are resource hogs (cpu and memory). - Clojure has better language semantics than ClojureScript: can define macros in the same compilation unit, not hindered by underlying javascript vm semantics. - Clojure is more performant: while modern js is actually multithreaded, and this multithreading is as easy to use as promises, which in turn super easy to use in core.async, js semantics require data serialization between threads, while JVM is able to share it. - both js and java library ecosystems are huge, you can find whatever library you need. java ecosystem is more mature: less breakage of maven itself, less breakage in individual libraries - industry support is better in js world. JavaFX is developed by small team at Oracle/Gluon. Cljfx is maintained by one guy who hangs out on clojurians slack all the time. Electron and js has much more stakeholders.

👍 8

Also, for very simple GUIs, Seesaw works pretty well:


@U47G49KHQ I think #1 should be investigated further. My gut feeling is cljfx wouldn't use as much CPU, and memory should be a constant overhead, but might be wrong.


memory is constant, but for small apps in electron it’s 100mb, and in cljfx its 800mb


study with n=1


I've had REBEL run with Xmx50mb


you can run cljfx with Xmx50mb


it will limit the heap size, but jvm internals will still use half a gig


Well, when I measured the process as well I never saw it go beyond 100mb


maybe that part can also be limited, but I haven’t investigated that further


I know, because my laptop only has 4GB RAM, and no swap 😛 I can't even run Google Chrome without it trashing


So I use Firefox, and can't use VSCode or IntelliJ, need to run Emacs


hmm, have you tried running cljfx/hn?


No, I should try


it could be that jvm limits it’s internal memory use depending on available memory…


Hum... REBEL is only delivered as a JAR though


And I have the Zulu JDK with JavaFx included


the differentiator for membrane is that many other gui libraries start with some underlying tech (javafx, the DOM, swing, cocoa, etc) and build up. membrane tries to take th opposite approach. think “what would be the ideal?” approach and build down. think it of it like ring for guis. the same ui code can the run on top of multiple implementations


so it’s in a weird spot where it’s not mutually exclusive with other frameworks and libraries


I think that approach might have some drawbacks: it has to be lowest common denominator, right?


btw, I’ve been recently checking out cljfx and it’s very cool


@U0K064KQV you could try clj -Sdeps '{:deps {hn {:git/url "" :sha "d3bbccb9fd6ebc6e278bdf0dc3f49351d826025c"}}}' -m hn.core


there’s nothing getting in the way if you want to have platform specific specializations


Might prefer that, also because you have a deb package and I'm on openSuse :S


and if we’re comparing it to the DOM in the browser and javafx, then we’re already in lowest common denominator territory


> there’s nothing getting in the way if you want to have platform specific specializations but then you are effectively tied to that platform?


or you need to reimplement this specialization for different platforms

👍 4

yes, that’s the tradeoff, but since the whole stack is in clojure, it’s a lot easier


hmm, I think that might be useful when you need to target multiple stacks at the same time


but i’m totally with you on getting a lot of push back from the crowd that’s used to the web


we’re on the same team!


Hum, so it was consuming 650mb for me on normal


And OOTO at Xmx100m


you mean OOM?


what’s OOTO?


At Xmx300m it seems to work, but ya, total memory in OS is still around 650mb


Now I'm wanting to double check rebl, maybe I have my data wronf


please do!


Hum, so REBL does run with Xmx100m, and doesn't OOM, but you're right, process memory is still around 600mb


thanks for verifying! I think cljfx/hn might OOM depending on the size of pages you open, since it additionally embeds a browser…


Ya, REBL is a pretty lgihtweight app in comparison to having to render web pages


I guess JavaFx is a bit of a resource hog 😛


Still planning on trying out cljfx


I think to me, a JVM based option does feel an easier environment then trying to do Electron


Just getting Java's api to interact with the OS


ok, going to bed, ttyl

👋 4
Ben Sless10:04:15

The last point in your comparison might actually be in favor of cljfx (only half joking). There are less people to help maintain the project, true, but also less people making a mess and deviating from the founder's vision


Haha yeah 😄 It’s not like I’m discouraging contributions though!


I've wanted to check out Cljfx for a while. It's a Java FX wrapper with a react-like API. Seems simpler to work with than dragging in all of Electron.


There was a Hacker News frontpage post about it a week or so ago, but I can't seem to find it.


If you go with JavaFX/Cljfx, you’re building a Java-based app in Clojure, but if you go with Electron you’re building a JavaScript-based app in CLJS. That can make a difference in what libraries you have available to build the rest of your app with, apart from the GUI.

👆 12

(like database engines, etc)


Thanks for the article @teodorlu. I'm probably going with Java, since I can't find a good reason to use Electron, but that decision could change.


I just realized that when clj-new generates a new app template, it puts a file in resources/.keep. What is this used for?


it is likely just there for git, because you can't check an empty directory into git


Specifically, with boot you got an error if a folder was on the list of paths to include but it didn't exist, so you had the situation where you'd create a project (with an empty resources folder) and it would all work locally, then you'd commit it to git and someone else would clone it -- and the empty folder would not be there because of git's policy on empty folders -- and then you would get an error trying to run the project.


It doesn't affect clojure CLI in the same way but it was easier just to keep it in the clj-new templates so that folks have a resources folder and the project is more "complete" as a starting point.


Is anyone familiar enough with who could explain to me how to wire it up to handle subcommands? I'm aware of the :in-order argument to parse-opts , and that my subcommand and its options will be returned in the :arguments key in the parse-opts response (which I'll have to re-parse) but it isn't super clear to me how (or if it is possible) to set up a subcommand on the initial parse-opts invocation. I'd like to show the subcommand in my top level usage summary.


actually after typing that out it dawns on me that I need to just write my own usage summary with my subcommands outlined


or a separate parse-opts config for the subcommands, then a "join" operation


and then just toss the returned arguments into another parse-opts invocation, with a separate set of options for the subcommand


the join op is a good idea as well - can I produce a usage summary from a given set of options without invoking parse-opts?


I saw there was a public summarize function, but that takes only a compiled option spec


I suppose just invoking parse-opts with an empty argument vector & your options and grabbing the returned summary works just fine, just seemed slightly heavy


> The default summary function `` is public and may be useful within your own `:summary-fn` for generating the default summary.


looks like it's worth checking at least


Given that there have been simultaneous questions about tools.cli in both #beginners and #clojure just now, that's definitely an indication I need to spend a fair bit of time expanding the documentation! 😐


I suspect adding several example programs (in the tests) would also help people out a lot...


It really doesn't help that it has two completely different versions of argument handling in one file either (because the deprecated legacy handling was never actually removed).


@seancorfield I think my unfamiliarity with the command line nomenclature is also not helping - arguments, options, argument options, etc etc


The sample program was a great help - I just realized I wanted to turn my required option into a subcommand 🙂


I think if there was some "post-parse" phase that supported additional validation in a standard way, it would help with a lot of these things, as well as better support for subcommands (which would recursively apply tools.cli on the :in-order arguments).


I haven't given that library much love lately but it was already pretty solid when I took it over in 2015...


(and most of the first release I did was just documenting what Sung had already implemented but had gotten around to releasing as 0.3.2)


Hi! I wrote cljfx, and I’ve been trying to make somewhat unbiased comparison between that and electron after I wrote Year of Clojure on the Desktop ( and received a lot of criticism from the js devs 🙂 In short: - both cljfx and electron are resource hogs (cpu and memory). - Clojure has better language semantics than ClojureScript: can define macros in the same compilation unit, not hindered by underlying javascript vm semantics. - Clojure is more performant: while modern js is actually multithreaded, and this multithreading is as easy to use as promises, which in turn super easy to use in core.async, js semantics require data serialization between threads, while JVM is able to share it. - both js and java library ecosystems are huge, you can find whatever library you need. java ecosystem is more mature: less breakage of maven itself, less breakage in individual libraries - industry support is better in js world. JavaFX is developed by small team at Oracle/Gluon. Cljfx is maintained by one guy who hangs out on clojurians slack all the time. Electron and js has much more stakeholders.

👍 8