Hacker Newsnew | past | comments | ask | show | jobs | submit | drikerf's commentslogin

I use it for Wobaka[1]. It's 98% Clojure and ClojureScript with some bash added. I even use it for CSS with a beautiful library called Garden.

Extremely happy going with Clojure!

[1] https://wobaka.com/blog/building-a-startup-on-clojure/


Nice project! I do wonder though if jsx is the best way to represent elements in code?

Clojure datastructures makes this so much more enjoyable. Everything is just basic lists and maps which makes it very flexible and powerful.

[:ul [:li "task 1"] [:li "task 2"]]

It's weird that it's not more common for making web apps.


There are a lot of DOM util libraries that look like

  h("ul", h("li", "task 1"), h("li", "task 2"))
This is called "hyperscript-style" after an early library that used it. This is basically what JSX compiles to too. There used to be a lot of JSX vs hyperscript debates.

There's also variants like h.ul(h.li("task 1"), h.li("task 2")) using Proxies now too.


There is a library for Python called htpy that does this.

Trouble is if you're used to HTML it can take a while to get used to it. It's like a learned helplessness or something.


AFAIK you should always leave space between floor and walls. As the material may grow/shrink with temperature etc. Very noticable in Sweden.


It’s also to prevent moisture issues. If you plaster right down to the floor then the plaster slowly soaks up moisture over time. Instead an unsightly gap is left to prevent this bridging. Skirting / baseboard covers the gap.

So much of what happens in house construction is about water management and without understanding it, it’s common for people to create issues. Regularly see patios that cover airbricks, causing suspended timber floors to rot.


Good times! I really enjoyed making a 90s version of my startup's landing page: https://90s.wobaka.com/


Similar story. I'm also running my SaaS on Clojure and maintenance has been a dream compared to other languages. Things rarely break and are easy to upgrade.

Developer experience is fantastic too with HMR, REPL and cljc files. It's fun to write Clojure code :)


What do you use for an editor/IDE? I'd love to write more Clojure, but the tooling story is a bit confusing


In my case, Emacs+clojure-mode+cider. I'm not sure I recommend learning Emacs at the same time as Clojure though. I usually recommend new users look to Cursive(for IntelliJ) or Calva (vscode) depending on what they are most comfortable with.


I'm a vim user and there's no way in hell I'm switching to emacs, even with EVIL mode. I'll check out Calva


I forget what the current recommendation for vim is, but back when I still wrote a lot of Clojure, people used vim-fireplace. I think there is a newer one for NeoVim, though


Almost ten years ago, I gave a lightning talk about Overtone (Clojure mappings for SuperCollider). I used "vim-fireplace" to connect to cider-nrepl and had a pretty good experience being able to quickly eval sexps in Vim.

I'm sure this is wildly outdated by now - I'd be shocked if there wasn't something halfway decent in the NeoVim ecosystem.

I will say, though, that during a brief flirtation with Spacemacs, I absolutely loved the Clojure integration - emacs just feels like it's more "at home" with lisps. Some of the most satisfying programming experiences I've had, honestly.


As a Vim user I agree. Spacemacs is an excellent Clojure dev environment.


The spacemacs / emacs feature I miss constantly in Vim / VsCode is "slurp/barf" - which is amazing useful in plenty of contexts that aren't s-exps.


Yeah, it’s annoying that paredit is “complected” with Clojure and lisp modes in other editors. This is one of the major things keeping me in emacs: it’s relatively trivial to mash up pre-existing emacs functionality ad hoc; other editors tend to precombine features in arbitrary ways.


I too resisted emacs for years while writing Common Lisp in vim, but eventually I ported my vim config line by line to the emacs lisp implementation of the vi standard


Checkout Cursive on Intellij. The $100 on Cursive is the best $100 I ever spent on a tool.


Author of Cursive is on Hn also.


Check out Conjure for nvim: https://github.com/Olical/conjure/


I'm currently a Conjure (and vim-sexp) user, but was wondering if there was something Clojure specific that didn't also have annoying tabbing defaults


Hopefully someone will chime in with their recommended setup for vim. I think there is pretty mature tooling available, but I'm not immediately familiar with it, so my advice there would be no better than Google.

Calva is a solid choice if you don't mind switching editors.


Emacs + CIDER and paredit. Works great!


HMR?


"Hot Module Replacement (HMR) is a feature in some programming languages and frameworks that allows developers to update modules or parts of the application in real-time while the application is running, without requiring a full page refresh. This is particularly useful during development as it speeds up the development process and helps maintain the application state."


Hmm I’d rather hear more from the GP (assuming it’s the same concept) and specific tools being used than a generic GPT sounding reply. Thanks though at least I learned an acronym (no snark).


I can't blame you for thinking it was a GPT reply these days. Anyway I'm also not GGP but he's right, it is Hot Module Replacement.

You don't really need specific tools for HMR in clojure, it's kind of baked in the language thanks to:

- immutability (not impossible but hard to do HMR without it)

- REPL workflow

When developing on clojure, there's a long-lived clojure process and you continually send expressions to be evaluated there.

Let's say you start a server, you make a change to one of the functions that handle a route, just after making the change you will evaluate that function (from your editor) and the update will be "live" in the running server.

It's honestly hard to describe and it doesn't help that we call that "repl-based workflow" when in fact we are not using the REPL directly, but rather through plugins available for various editors. People might think it's the same as using nodejs or python repl, when it's nothing like that. If you're interested, you could check "Parens of the Death" or look for "Clojure CALVA" (VSCode's clojure plugin) videos on youtube.

https://www.parens-of-the-dead.com/s2e1.html

Edit: It is also not limited to development. Nothing prevents you from hooking into the clojure process in your prod server from your editor, editing a function and evaluating it.

You just monkey-patched a live server to fix a time-critical bug with roughly the same workflow as editing your code locally. Or brought production down, 50/50, but what's life without some risks eh?


I have a lot more experience with common lisp, but currently doing AOC with Clojure right now to try and position myself better for my dream job where I can work with any kind of lisp. Thought I could maybe ask a quick question I have been having trouble answering? I find that with emacs/cider any evaluation is blocking. I am a little spoiled on SLIME/SLY, where it has the nice baked-in feature that any evaluation you do interactively becomes its own process and you can carry on evaluating other things if the process is long running. Is there any way I can replicate this in clojure? Is it just a matter of using `future` explicitly? Or is there something I am missing with cider?


If you want a long running calculation to not block the REPL you'd need to use a future, or some other method to get it off the REPL thread.

I don't remember what your describing being a feature of SLIME, but it's been 10+ years so I may just not remember.


I don't think you are missing anything, I am not aware of anything that would automatically launch new threads when evaluating something.

That does seem like an interesting, although in practice I rarely have to evaluate something that would take more than a a couple of seconds, so manually wrapping it in a future seems acceptable.

On a semi-related note, sometimes I mistakenly evaluate something that will take a long time, or there's a bug and I'm stuck in an infinite loop. CIDER has a command to interrupt the on-going evaluation but I'm my experience it only works about half the time. In those cases having it wrapped in a future would certainly be helpful.


Just for the record, in Java (JVM) this is done (routinely circa first 'bean containers' of ancient history) via interfaces and class loaders. Using Java I can have n distinct versions of a class each implementing the same interface in the same process as long it is the interface reference that is passed around.

> immutability

Make a case for immutability being so critical to HMR. How does an immutable data allow for swapping of code.


AFAIK, Java's native HMR is not built on dynamic classloaders, but using a separate technology built into the JVMs debugger support, called HotSwap. You can only replace (certain) function bodies with it. To get around these restrictions, more elaborate HMR can be achieved with classloader magic, such as OSGi, but you subscribe to a world of ClassCastException-pain with it.


It is still a very good system, and you can get quite a far away with only method-swaps, I love to use them in case of Spring apps, where changing a backend controller and doing another api call is a very fast REPL cycle basically.

But objects don’t give themselves very easily to hot reloading, there are a bunch of edge cases that are not well-defined in a semi-swapped state (e.g. this new field always gets a value in the constructor, but was no just added. Should you rerun the constructor, or have it be in an inconsistent state?)

Immutable/FP concepts are a much better fit, especially if the building blocks are as small as they are in case of Clojure.


My limited understanding of Clojure's HMR is that there aren't n distinct versions of a class/function. It's actually removing the old ones from memory and replacing them.

Also, it is worth noting that everything is immutable, not just data. Functions/protocols/records/etc are also immutable.

Functions/classes being immutable makes it much, much easier to reason about the dependencies of that function/class. E.g. check out this Python code:

    import someotherpackage
    import threading
    import random

    class MyLogger:
        def __init__(self, name, path):
            self.path = path
            self.name = name
        def log(self, message):
            with open(self.path, 'w') as fd:
                fd.write(message)

    my_logger = MyLogger("init-logger", "init.log")
    someotherpackage.logger = my_logger
    someotherpackage.logger.error = print

    print = someotherpackage.logger.error
    my_logger.thingy = "thingy"

    def change_logger():
        def actually_do_change():
            time.sleep(random.randint(1, 100000))
            new_logger = MyLogger("main-logger", "main.log")
            someotherpackage.logger = new_logger
            someotherpackage.logger.error = my_logger.error
        time.sleep(random.randint(1, 10000))
        threading.Thread(target=actually_do_change).start()

    time.sleep(random.randint(1, 10))
    threading.Thread(target=change_logger).start()
Now imagine that this isn't our `main`, this is some library that's a transitive dependency of something we're importing.

Trying to hot-swap this would be awful. The big issues related to mutations are that a) there's no indication in someotherpackage that it's behavior depends on this package, and b) because mutations are allowed, the order that things are reloaded in matters, and c) some mutations are time-bounded.

If I change MyLogger and the VM/interpreter wants to hot-swap MyLogger, it has to recognize that it can't just re-load the instances of MyLogger with the same state. It has to re-load those instances, then re-mutate them, then re-mutate someotherpackage, and finally re-mutate the print function. It might also need to do the stuff that's delayed by that thread. Maybe. Depends on a couple of random integers that probably got GCed a while ago.

If you want to add an extreme layer of annoying, consider that this package could be a late import and only happens if a plugin is enabled, so the earlier code might rely on `print` actually being `print` or on someotherpackage.logger behavior that changes over time.

None of that applies in an immutable world. Nothing can mutate the someotherpackage package nor the `print` function, ergo nothing can depend on an earlier or different version of them. Dependencies are easy to track because the only way to introduce them is to import/directly reference them, or have them passed as parameters.

I don't know that it even needs any dependency tracking, though (provided they're using a pointer to a pointer, or maybe something smarter than that I can't think of). Immutability means that a) everything can be passed as pointers safely, b) those pointers can't be modified by the code, and c) there is never a reason to have more than one copy of a pointer to the bytecode for a function.

Each function gets a pointer to its bytecode, let's call that p1. Every reference to that function is a pointer to p1, which we'll call p2.

When you want to hot-swap code the language pauses the VM/interpreter, recompiles any function with a diff (easy with an AST), puts the new functions in memory somewhere new, changes p1 to point to the new bytecode, then marks the old bytecode for GC.

If you tried something that simple in Python for the code I wrote above, it would explode. It's an infinitely harder problem.

> Using Java I can have n distinct versions of a class each implementing the same interface in the same process as long it is the interface reference that is passed around.

It's been a while since I worked with those app containers, but from my recollection it shares virtually nothing with hot reloading. E.g. is state preserved between those? If I'm caching stuff in RAM, and I change that class out, does it keep the cache? HMR does, as I recall.

My understanding is that app containers basically just run N versions of your app/class and allow you to choose which one you route execution to.

App containers are more similar to blue-green deployments with a load balancer. Each instance is totally separate, and the load balancer lets you choose which one you route to. App containers just do that process inside the JVM.

That's not bad, but it's also not as good as being able to repeatedly tweak a function without ever clearing caches or re-connect to downstreams or etc.


> My limited understanding of Clojure's HMR is that there aren't n distinct versions of a class/function. It's actually removing the old ones from memory and replacing them.

It is almost both. Clojure creates n distinct versions of the function (which are in fact objects and subject to garbage collection). The symbol of the function is rebound (technically this language is wrong) to point to the most recent one. Then, usually, the Java garbage collector deletes the old object.

So it is possible (likely under some code styles) that old versions of a function can hang around in a REPL environment if they ended up embedded in a data structure. However, if you make the call by resolving the function's symbol then that will reliably call the most recently def-ed version.

For example:

(defn poor-style [] 1)

(def object {:fn poor-style :call (fn [] (poor-style))})

(defn poor-style [] 2)

(prn ((:fn object))) ; => 1, calls the old embedded function

(prn ((:call object))) ; => 2, calls a function that resolves the symbol, finds the new version.

(prn (poor-style)) ;=> 2, just call the new function


Fwiw it's a quote from https://webpack.js.org/concepts/hot-module-replacement/, what else would you like to know about HMR?


It's actually the answer ChatGPT should give, with the citation to back up the quote.


if only.... lol


Thanks all for the various answers. I guess, coming from common lisp where it's easy to add a new dependency (via asdf / quicklisp) to a running REPL, it's not so easy (yet? until 1.12?) with Clojure. Various utils have been created over the years, so was just wondering if there was something more to the picture that fell under this new term I learned, "HMR"..


If you want better answers, ask better questions.


Have you ever used a dictionary?


Apologies for the tone. Was really tired when posted.


Any functionality that is tested :)


"if it's not tested, it's not functionality"


Hyrum would want to have a word...


If only there was a way to run all the tests of all the software that depend on your change...

Fwiw, that's what monorepos are good for.

Sadly it's hard to make a "world monorepo"


If you pin your dependency versions and the dependency maintainers run tests before releasing, you're already almost there (excluding bugs that might arise only in your specific environment). If dependencies don't have tests or test automation, you can always contribute them.

As for the platform-specific bugs, monorepos only help if the way to run tests in all the components is standardised. But this is something you can just as easily implement across repos. Could be as simple as having a testme.sh in the root.


> > "if it's not tested, it's not functionality"

This suggests you may have not 100% test coverage in your tests. But 100% coverage if what? What is the specification you're defining your behaviour against?

The comment above suggests that you could treat your tests as if they were the ones that actually define your contract.

> Hyrum would like to have a word.

This is a reference to "Hyrum's law" which says:

"With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody."

This comment, in response to the previous about defining tests as the source of truth for your contract, remarks that sadly you can't do that because no matter what contract you wish you define, ultimately the behaviour of your existing software becomes its effective contract.

> [My comment about monorepos]

Here I suggest that if you extend the notion of what is the test corpus to include the test corpus of all of the software that depend on you (not your dependencies! The code whom your code is a dependency) then you could detect if a (yet unmerged) change you're making is actually going to affect any existing code.


Totally misread the "depend on you" as "you depend on" part, sorry.

Interesting idea, but monorepos still don't help on their own. You need a way to detect which modules depend on yours and a way to run and interpret their tests. Whether they're in an adjacent folder or another repo changes very little.

GitHub actually already has a dependency scanning thingy that builds cross-repo dependency graphs [0] and package managers have been doing that for years. The missing parts wouldn't be that much effort to build, but getting everyone to set up their repos to work with it would probably be prohibitively difficult.

[0] https://docs.github.com/en/code-security/supply-chain-securi...


Monorepos are useless unless you have good tooling, including a build system that is aware of module dependencies inside the monorepo.

An example build system that can do that is https://bazel.build


Correct. Another interesting one:

https://github.com/just-buildsystem/justbuild


That's exactly what the Nixpkgs Hydra instance does—caveat being your library and its consumers all need to be in Nixpkgs. I'm hopeful that there will be a way to keep the tracking part of that going forward, as Flakes are adopted and the community decentralises.


Why? Just curious. I've found the JVM to be surprisingly stable and great to work with.


Super long startup times and large binary size are what turned me off. This is clojure specifically not jvm specific. I find the JVM to be fine, but when I looked for another lisp I did want one that was compiled just because it was easier to distribute my programs.


You can compile it to a native binary, then you get super fast startup times and small-ish binary sizes.


I invite you to try it. Put together a modest CLI that does SQLite, some network calls, and unzips files. ( https://github.com/djhaskin987/zinc ). Using native-image with any reasonable set of dependencies like this is *horrendous*. Just because you can doesn't mean it's tractable. I spent 10% of my time writing the tool and 90% of it trying to get it to compile. Absolutely the worst experience trying to get something to build in my life, and I'm a devops engineer. Building and shipping code is my thing.


Ya, I'm not going to disagree, it's not the nicest build pipeline.

That said, you can figure it out normally.

Using native dependencies will always be the hardest. I'd recommend first trying to use graalvm friendly libraries, and if not, libraries that are pure Java and don't have native dependencies.

For SQLite for example, you have to include the SQLite C driver, and that's where it gets a bit complicated.

See here for a demo build that includes SQLite: https://github.com/ericdallo/sqlite-graalvm-sample/tree/mast...

The most friendly way is to rely on this: https://github.com/clj-easy

It lists Clojure native image friendly libraries you can use. And it also includes some pre-configured dependencies you can depend on that will bring the correct build config.

Then just pretend like there are no other libraries yet for this "new" language. Or learn about the native image process more deeply and contribute to the effort to add easier support for more libraries.


I love Clojure but the JVM is admittedly resource hungry.


I haven't found that to be an issue. For small programs there's also babashka.


Don't know about your exact environment but I've structured my code so most of it is in .cljc files which can be evaluated in browser and on server. This allows me to evaluate/test/run things easily in the emacs repl.

For view code I think hot reloading has been a great thing with Clojure/Script too. Everything is reloaded properly and the state remains.


You can have a cljs repl as well, and you can open an alert in the browser from it like you'd do in the console


Wow, thanks! Recently migrated and seems something went wrong. Fixed now :)!


Clojure is great for writing backends. Checkout Ring and Compojure. Simple libraries for making an API :).


Will check those, thanks.


Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: