Try Clojure
tryclojure.org
tryclojure.org
I use Clojure nearly daily at my job and at home. Sometimes it's standard Clojure, sometimes it's the excellent Babashka flavor which I use as a make-like task runner and Zsh-like script replacement. It's not the only language I use of course, and Go is a strong second place most of the time especially if I need something compiled to a single binary. But Clojure is where I generally feel most at home thanks to the irreplaceable REPL based development flow which is more like a dialogue with my program than your typical write compile run loop.
Combine that with it running on the JVM and you have a wonderful set of tools to get things done in a pleasant way.
I strongly encourage anyone with even a passing interest in Lisp and functional programming to give it a try. If you're using VSCode there is the excellent Calva plugin to help you out.
The creator, Anthony Grimes (https://github.com/Raynes), sadly passed away seven years ago: https://www.reddit.com/r/Clojure/comments/5gyyxw/clojure_ope...
To me, switching to a less expressive language is painful and infuriating. I remember having to switch from Python to Java 5, and how everything started to take 3 to 5 times longer code to express.
Maybe the key thing is to only write small things in less expressive languages, like shell scripts, or small C functions to help performance or FFI, or, well, tiny Go utilities.
There's a population of people that don't like this kind of abstraction, because to understand the code you first need to understand the problem, the data flow and how it's expressed. They prefer to look at the issue "bottom up" than "top down", and for them Go is perfect, because instead of reasoning about what the module does, how the data flows, what the algorithm does, they can focus on the small: this is a loop that iterates through a slice, this is an if condition that depends on this variable.
There's no better or worse here, just two types of people. Some like to look at the whole bridge, some like to examine the individual nuts and bolts and joints.
No, two types of solving a problem. I like writing both (Haskell and Go) - for _myself_ - but the tooling and standard library and ... of Go is orders of magnitudes better.
Then a week later, the squinty mole discovers the code is dead; not called at all. There is no slice and no variable.
How does this affect readability when one tries to dive into an existing Go project? Is Go code harder to navigate than code in more expressive languages?
You can get incredible mileage out of Go using just the stdlib, built in tools, and the pure go port of SQLite. With Clojure it feels like I have to stitch a million random libraries together to get anywhere. Writing Clojure is delightful but the ecosystem and tooling pale in comparison, and at the end of the day I think that’s what matters.
If Clojure had great built-in tooling, better error messages (and maybe some SBCL-esque type checking features), and if Datomic was open source and less rough around the edges, I think the language would have been far more successful.
Making historical interfaces well-typed is another kettle of fish. For instance, Python's range() is actually two overrides with incompatible signatures. I bet PyTorch has more of such examples.
an expressive language (clojure, python, C#, etc.) is good when you're iterating "what problem am i even trying to solve?"
a less expressive language is typically better when you more or less know a high level solution to your business problem, and you're iterating on "how do i make the machine actually do it". like "how do i reduce my AWS bill" or "how do i render it in 16ms?"
for something like gamedev, enough of the problem is in the second category that you might not even reach for the expressive language. (many do, though. with lua, or artisanal lisp dialects)
To me it isn't more infuriating than learning a new language or practicing one I'm not very good at. There's a bit of friction, but that's common when I'm developing in the languages I'm most fluent in too. Either that or the problem is trivial and likely to be in most common programming languages.
If you are making JavaScript look good you are failing as a language.
(Say what you want about Oracle Corporation, but as the wardens of Java they're doing a pretty good job.)
Might still end up marrying only one though!
C++ or Rust are expressive in both directions, towards the metal and upwards in terms of abstractions. But those languages are not easy to reason about. They come with lots of stuff, hard to grok abstractions, ceremony and so on.
Small languages like Scheme or Lua are expressive in that they don't force you to write a lot of stuff to get the job done. But those languages don't typically let you express what the computer should actually do at a lower level.
C or Zigs let you do those things and are also relatively small languages. But their facilities for abstraction are limited and they force you to express more computational details.
Go frees memory and schedules goroutines for you, but lets you express a lot more about how you lay out memory than other managed languages, so you can actually think in terms of bits and bytes. It has a great std lib so you can get stuff going quickly and reliably.
But sometimes limitations can spur creativity.
It's pretty easy to switch to writing JVM Clojure if you're familiar with Babashka. Most of the libraries written for Babashka are designed to work in either environment.
That said, there are reasons you may want to use the Clojure on the JVM later on. It might be interesting to read the replies to another poster with similar concerns about the JVM: https://news.ycombinator.com/item?id=40445415
It's a particularly good entry point because unlike full-JVM Clojure it has a very fast startup time. Newcomers can use any file-watching /reloading tools (e.g. nodemon) that they're already familiar with to work with it interactively.
Hopefully, a enthusiastic user will graduate to using a REPL connection in their editor for a fully interactive setup. But newcomers tend not to do this... its an unfamiliar workflow to most, and can be pretty cumbersome to setup.
[0]: https://babashka.org
Best part is that bb can start nREPL with `bb --nrepl-server` and then you can connect an editor like Calva to it and develop the script interactively. Definitely recommend checking it out if you need to make a simple web UI. Here's an example of a full fledged web app:
#!/usr/bin/env bb
(require
'[clojure.string :as str]
'[org.httpkit.server :as srv]
'[hiccup2.core :as hp]
'[cheshire.core :as json]
'[babashka.pods :as pods]
'[clojure.java.io :as io]
'[clojure.edn :as edn])
(import '[java.net URLDecoder])
(pods/load-pod 'org.babashka/postgresql "0.1.0")
(require '[pod.babashka.postgresql :as pg])
(defonce server (atom nil))
(defonce conn (atom nil))
(def favicon "data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA/H8AAPxjAAD/4wAA/+MAAMY/AADGPwAAxjEAAP/xAAD/8QAA4x8AAOMfAADjHwAA//8AAP//AAA=")
(defn list-accounts [{:keys [from to]}]
(pg/execute! @conn
["select account_id, created_at
from accounts
where created_at between to_date(?, 'yyyy-mm-dd') and to_date(?, 'yyyy-mm-dd')"
from to]))
(defn list-all-accounts [_req]
(json/encode {:accounts (pg/execute! @conn ["select account_id, created_at from accounts"])}))
(defn parse-body [{:keys [body]}]
(reduce
(fn [params param]
(let [[k v] (str/split param #"=")]
(assoc params (keyword k) (URLDecoder/decode v))))
{}
(-> body slurp (str/split #"&"))))
(defn render [html]
(str (hp/html html)))
(defn render-accounts [request]
(let [params (parse-body request)
accounts (list-accounts params)]
[:table.table {:id "accounts"}
[:thead
[:tr [:th "account id"] [:th "created at"]]]
[:tbody
(for [{:accounts/keys [account_id created_at]} accounts]
[:tr [:td account_id] [:td (str created_at)]])]]))
(defn date-str [date]
(let [fmt (java.text.SimpleDateFormat. "yyyy-MM-dd")]
(.format fmt date)))
(defn account-stats []
[:section.hero
[:div.hero-body
[:div.container
[:div.columns
[:div.column
[:form.box
{:hx-post "/accounts-in-range"
:hx-target "#accounts"
:hx-swap "outerHTML"}
[:h1.title "Accounts"]
[:div.field
[:label.label {:for "from"} [:b "from "]]
[:input.control {:type "date" :id "from" :name "from" :value (date-str (java.util.Date.))}]]
[:div.field
[:label.label {:for "to"} [:b " to "]]
[:input.control {:type "date" :id "to" :name "to" :value (date-str (java.util.Date.))}]]
[:button.button {:type "submit"} "list accounts"]]
[:div.box [:table.table {:id "accounts"}]]]]]]])
(defn home-page [_req]
(render
[:html
[:head
[:link {:href favicon :rel "icon" :type "image/x-icon"}]
[:meta {:charset "UTF-8"}]
[:title "Account Stats"]
[:link {:href "https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css" :rel "stylesheet"}]
[:link {:href "https://unpkg.com/todomvc-app-css@2.4.1/index.css" :rel "stylesheet"}]
[:script {:src "https://unpkg.com/htmx.org@1.5.0/dist/htmx.min.js" :defer true}]
[:script {:src "https://unpkg.com/hyperscript.org@0.8.1/dist/_hyperscript.min.js" :defer true}]]
[:body
(account-stats)]]))
(defn handler [{:keys [uri request-method] :as req}]
(condp = [request-method uri]
[:get "/"]
{:body (home-page req)
:headers {"Content-Type" "text/html charset=utf-8"}
:status 200}
[:get "/accounts.json"]
{:body (list-all-accounts req)
:headers {"Content-Type" "application/json; charset=utf-8"}
:status 200}
[:post "/accounts-in-range"]
{:body (render (render-accounts req))
:status 200}
{:body (str "page " uri " not found")
:status 404}))
(defn read-config []
(if (.exists (io/file "config.edn"))
(edn/read-string (slurp "config.edn"))
{:port 3001
:db {:dbtype "postgresql"
:host "localhost"
:dbname "postgres"
:user "postgres"
:password "postgres"
:port 5432}}))
(defn run []
(let [{:keys [port db]} (read-config)]
(reset! conn db)
(when-let [server @server]
(server))
(reset! server
(srv/run-server #(handler %) {:port port}))
(println "started on port:" port)))
;; ensures process doesn't exit when running from command line
(when (= "start" (first *command-line-args*))
(run)
@(promise))
(comment
;; restart server
(do
(when-let [instance @server] (instance))
(reset! server nil)
(run)))shadow-cljs[2] makes using npm libraries easy.
i’ve settled on go backends and reagent frontends as my default setup[3].
1. https://reagent-project.github.io/
Although it seems to catch up with experimental support of React 18 now, Reagent has fallen behind the latest developments in React and may not benefit from all of its performance optimizations. It is still using class components instead of hooks and there have been concerns that the runtime conversion of Hiccup may drag down performance. I guess in most cases it is not really an issue or in any way noticable, so if you’re not doing any fancy stuff it should be fine. I may even come back to Reagent at some point, since I have to admit that I miss the UI-as-data model with Hiccup.
What I highly recommend, however, is using re-frame[2] for state management. It has also been around for a long time (2014, around the same time Reagent came along) and pioneered some popular ideas in that area. It may seem a bit overwhelming at first, but the docs provide a great introduction and I find the model very clear once you wrap your head around it. At the moment it depends on Reagent, but there are ways around that. [3]
[1]: see Helix (https://github.com/lilactown/helix) or UIx (https://github.com/pitch-io/uix)
[2]: https://day8.github.io/re-frame/
[3]: refx (https://github.com/ferdinand-beyer/refx) is an almost drop-in replacement without the Reagent dependency, but hasn’t been updated in a while. Alternatively, re-frame can be integrated with UIx/Helix by adding some interop code https://github.com/pitch-io/uix/blob/master/docs/interop-wit...
i don’t even know what the difference between hooks and classes are, reagent has never given me a reason to care.
if that’s not simplicity, i don’t know what is.
if i were to build a 3d high performance game in react, maybe i’d experiment with dropping reagent. instead i’d just use cpp.
not sure why people want yelp complexity websites to be 3d games.
i love react! i absolutely do not care about the details. never have…
this setup is a single go binary that serves a single pre-gzipped html file from a lambda.
that html file contains inlined js, that is a reagent app.
there is also a favicon. total of 3 files in the lambda zip.
you desired setup is certainly possible though. you’d probably want clojure on the backend though, so you could more easily do SSG.
[0]: https://www.braveclojure.com/clojure-for-the-brave-and-true
For me, the biggest benefit is that it's hosted. Learning only Clojure, I can easily today write for JVM, .NET, JavaScript, Flutter, or shell scripts. Even when I need to write Lua, I'd usually pick Fennel. It's not Clojure but feels very similar. There are libs that can give you Python or R interop from Clojure. There are projects to target Golang, Rust or Erlang. Jank is a super interesting, experimental implementation of Clojure that runs on LLVM, I'm very excited about it. Do you want to become a true polyglot programmer? You only need to learn Clojure.
Isn't CL better for this? You are not bound to some VM and you can produce native binaries.
Clojure is interesting, but some Clojure APIs (stuff like Spectre) is "I want this everywhere now" stuff.
But: you can also build regular Clojure programs using GraalVM and create a fast-starting binary.
jank uses immer for that: https://github.com/arximboldi/immer
Try Clojure – An interactive tutorial in the browser - https://news.ycombinator.com/item?id=30423856 - Feb 2022 (93 comments)
Try Clojure in your browser - https://news.ycombinator.com/item?id=3366526 - Dec 2011 (26 comments)
Try Clojure - https://news.ycombinator.com/item?id=1359682 - May 2010 (60 comments)
There is Python on the "other side" of the ETL pipeline, but everything user-facing is (again AFAIK) Clojure on the backend and TypeScript in the Android and iOS apps.
Instead, this fact always seems glossed over, and because of it anyone who spends 60 seconds writing lisp by hand into a repl assumes that's the lisp experience and nopes out forever. When in fact paredit makes lisps the easiest-to-edit languages in the world.
That is true and whenever I switch from Clojure to C, I immediately feel the pain (what do you mean I can't just kill or lift this entire expression?).
But structured editing has a learning curve, so it's a difficult balancing game. Beginners are overwhelmed by all the parentheses, while experienced programmers don't even notice them and love the structured editing approach.
Cool: https://shaunlebron.github.io/parinfer/
I always thought that the surrounding tooling (having to learn how to edit parens productively, using nrepl/cider, maybe even emacs... with evil-mode of course) to be both the worst and then eventually the best parts of Clojure.
It's surprisingly excellent! Sure, the "language" of paredit features more powerful text manipulation that just simple movement... but combined with the new "jumping" in the latest Helix release [1], it makes for a very impressive keyboard-based navigation system.
[0]: https://helix-editor.com [1]: https://helix-editor.com/news/release-24-03-highlights/
(my-name "<img src='#' onerror=alert(1) />")
map, filter and reduce are extraordinarily powerful, especially when combined with the other core library functions.
Is functional programming too weird for majority of computer programmers? Is functional programming not optimal for solving industry problems? Maybe there is no functional programming that that is up to some set of standards? Is functional programming too complicated?
I do enjoy functional programming myself, and while I don't use a functional programming language at work, I try to use functional programming paradigms when I finds it suits the probem.
Clojure is successful, stable and well maintained. However it is a niche language compared the big mainstream languages. It takes a functional approach by default, but doesn't shy away from other paradigms/patterns/idioms when appropriate.
Functional programming itself has had a huge influence on mainstream languages since about a decade and a half. More and more pragmatic features have been introduced in mainstream languages. Newer languages have adopted functional idioms from the get go. On the macro level as well, ops and architecture have been adopting statelessness, reproducibility and so on.
The consensus has shifted towards containing state. The more moving parts you have, the harder it gets to reason about a whole thing. Treating data as data.
OOP has also changed and got refined over this time. I think people realized that it has good ideas (generic interfaces, polymorphism etc.) and bad ideas (inheritance, local state etc.). Same thing with FP, there's stuff that's esoteric and too far from the reality of actual programming and stuff that is pragmatic and simplifying in there.
Higher order functions, immutable data structures, declarative UI frameworks, etc are things you're quite likely to encounter in a contemporary codebase these days.
(let ((a 1)
((b c) (list 2 3))
(d e (values 4 5)))
`#(,a ,b ,c ,d ,e))
;; => #(1 2 3 4 5) (let [a 1
[b c] (list 2 3)
[d e] [4 5]]
`(~a ~b ~c ~d ~e))
;; => (1 2 3 4 5) (let ((quotient remainder (floor x y)))
(+ (* quotient z)
(* remainder (floor z y))))
Where floor is a function which returns multiple values rather than a list or vector of results?Also, but this is entirely on me being an absolute dullard, I like having the brackets around each binding as a kind of guardrails for my mind to not lose track of where I am and what belongs where.
(let [[quotient remainder] (floor x y)]
(+ (* quotient z)
(* remainder (first (floor z y)))))
Because in Clojure there's no multiple value bind, when you want to return many results from a function, you wrap it in a vector, and that means you than just destructure that vector. That also means, it's not smart enough to know if it's used in a single value context or multi value context, so you need to always get the value out of the vector, which is why I call first in that second call to floor.=> (+ 2 2 + 3)
"4function Cg(a){switch(arguments.length){case 0:return Cg.s();case 1:return Cg.g(arguments[0]);case 2:return Cg.h(arguments[0],arguments[1]);default:for(var c=[],d=arguments.length,e=0;;)if(e<d)c.push(arguments[e]),e+=1;else break;return Cg.j(arguments[0],arguments[1],new gc(c.slice(2),0,null))}}3"
Babashka cli returns this, telling you clearly what the error is:
user=> (+ 2 2 + 3)
java.lang.ClassCastException: clojure.core$_PLUS_ cannot be cast to java.lang.Number [at <repl>:1:1](if (= 2 2) :OK :nope)
On the other side I've felt a lot of the ecosystem work that was done has a more "timeless" quality. Coupled with java interop I haven't ever felt wanting when I do reach for it for some hobby projects I keep up with.
One thing I will say, since it takes a different tack and philosophy, I think any programmer learning some of it benefits from the different perspective.
There was an almost apologist attitude towards rough edges or failure modes folks new to the community would fall into. I managed to work with it for a time despite that, but it left me with a bad taste in my mouth. It's really important to make your ecosystem and communities welcome to new users or they're destined to fade.
> ...had a spike in popularity despite being very hostile to newcomers.
To be fair the community itself, as humans, was very welcoming to newcomers. Really super nice and helpful folks.If anything was "hostile" it was the dominance of emacs and very limited options for other tooling. That, and the horrifically unhelpful leaky abstractions in the stack trace when something went wrong. It's a big step, if you don't already know emacs to learn emacs (at a fairly advanced level) AND a new very different programming language.
I see now there's a vscode option with Calva. I haven't tried it. Might do that someday. Really wanted to like this language!
The official Clojure CLI, for example, is just plain confusing, and that's most people's first impression to the entire ecosystem. The config files, while they use the wonderful `.edn` format, are also not intuitive. Newcomers are the most likely people to encounter the most amount of error messages... which are famously difficult to read.
And that's before you even get into REPL configuration, which involves coordination with your editor process, a client process, and a server process. Even if you have a tool like Calva or CIDER managing it for you, you'll still get confused when something goes wrong unless you grok all the moving parts.
Even if you figure that all out, you still don't have Parinfer or equivalents setup yet in your editor. Also, clojure-lsp tends to require some configuration to get working the way you want. And that's before you get started with ClojureScript, which brings the complexity to another level entirely.
Despite all this, I love Clojure. It's an expert's language, even though it shouldn't have to be. Once you learn this stuff, you respect why much of this complexity exists. It's inherent to the amount of power the language gives you.
But doesn't mean we can't make it easier to use and get started with.
There is one cultural thing that might be confused with unfriendliness. Sometimes people react badly if someone posts incorrect information. But I think that's good. When you search for information about Python or PHP you have to wade through quite a bit of junk. Ironically, it's sometimes easier to find correct answers for Clojure.
Clojure itself is very clean and consistent, it's got a lot of polish to it, which makes it comparatively easy to learn. And there isn't that much of it. But for a long time the tooling was hard.
That's far less of a problem than it used to be. deps.edn and shadow-cljs both made things easier, as has Cursive. People say nice things about Calva, but I don't know it.
I'm a big fan. Babashka alone is enough to make learning Clojure worthwhile. Also, for someone like me, it's kind of nice that it feels almost finished. Once you learn it, you know it, and now that the tooling has settled down a bit you don't have to keep running to keep up.
i tried calva with vscode afterwards but it was basically the same thing, calva has paredit which overrides the keyboard navigation and there was no way to disable it. somebody had asked for the option to disable it on the github but the creator said something along the lines of calva without paredit wasn't the way he wanted it to be used, so i just picked another language to learn.
https://github.com/clojure-lsp/clojure-lsp
https://github.com/clj-kondo/clj-kondo
I've always used (Neo)Vim with Clojure.
Some of this comes from lisp's minimalist syntax, which makes it hard to even know what kind of thing you're looking at when you're encountering a new thing for the first time. But the problem is also compounded by some of Clojure's more distinctive (and powerful) features such as ad-hoc polymorphism and using maps to pass function arguments.
The closest analogy from imperative languages that I can think of is another language that's famous for being incredibly productive in the hands of a skilled user, but whose code tends to feel kind of write-only: Perl.
Evidently, 93% paint splatters, when OCRed, are valid Perl programs:
https://www.mcmillen.dev/sigbovik/2019.pdf
I doubt that a similar thing is true for Clojure.
Here, the only problem is that you don't know what the symbols and some of the notations mean. You can look at the unfamiliar syntax and know what is a child of what. Most operators are words you can search for.
Because there's a preference for just passing immutable data around the time travel debugger really helps me understand complex interactions
Clojure is particularly vulnerable to that because it really is an enterprise applications language. That makes it more vulnerable to learning curve problems than a language like Rust whose most direct competitors are other languages with comparably steep learning curves.
nailed it. I don't know much about functional languages other than a haskell course back in the day, but I don't want to do functional all the time. It plays great in some situations and I am happy to use those language subsets in my daily coding, but I don't need my entire application to be written in an esoteric language
That is working under the assumption that “the benefits” are actual benefits.
IMO, “the benefits” are measurably drawbacks, and not a technical debt I am willing to accept for no good reason.
in my opinion competes with python when it comes to DS/ML. I find it a lot more comfortable to use if you use emacs bindings
It uses XTDB by default but you can switch to Postgres: https://biffweb.com/p/how-to-use-postgres-with-biff/
People don't really use ORMs in Clojure, they just write SQL directly and abstract the details from consumers using functions. That said, HoneySQL is a common alternative to writing SQL that makes it a lot less painful (and composable!): https://github.com/seancorfield/honeysql
(ns rads.sql-example
(:require [next.jdbc :as jdbc]
[honey.sql :as sql]
[clojure.string :as str]))
;; DB Table: posts
;; +----+-------+
;; | id | title |
;; +----+-------+
;; | 1 | hello |
;; +----+-------+
(def ds (jdbc/get-datasource (System/getenv "DATABASE_URL")))
(defn row->post [row]
;; This is your optional mapping layer (a plain function that takes a map).
;; You can hide DB details here.
(update row :title str/capitalize))
(defn get-posts [ds]
;; Write a SQL query using Clojure data structures.
(let [query {:select [:*] :from [:posts]}]
;; Run the query.
(->> (jdbc/execute! ds (sql/format query))
;; Convert each raw DB map to a "post" map
(map row->post))))
(println (get-posts ds))
;; => [{:id 1, :title "Hello"}]Some will say this is a good thing (and as a very experienced engineer I will agree) but the barrier to entry for a newbie is quite high in this regard. You have all the rope to hang yourself with - and you will. Especially if you are on a team full of lots of interns and junior folks.
webserver: ring-jetty9-adapter[0]
routing: reitit[1]
html templates: hiccup[2]
never used an ORM, but happily used HugSQL for making composable queries[3].
[0]https://github.com/sunng87/ring-jetty9-adapter [1]https://github.com/metosin/reitit [2]https://github.com/weavejester/hiccup [3]https://www.hugsql.org/
I have no doubt that XTDB is good, it is just surprising as a first choice and is not going to be easy to get up and running compared to the conventional alternatives
(Is there even a hosted offering for XTDB? I like that it's open source but seems that you will have to grapple with all this stuff including a Kafka cluster https://docs.xtdb.com/guides/starting-with-aws.html)
Plus, there are not a lot of semi-colons and commas in JS and Python.
Of course, there are constructs such as `let` and `cond` that are more parenthetically noisy (not so much in Clojure though), but on the flip side you don’t have to remember a lot of special syntax like in non-lisp languages.
Most Lisp-people also use structural editing tools like paredit[1], which make it really easy to write and edit s-expressions. I found that after some time I didn’t really think that much about parentheses anymore.
[1]: https://paredit.org
(defn fizzbuzz [n]
(cond
(zero? (rem n 15)) "FizzBuzz"
(zero? (rem n 3)) "Fizz"
(zero? (rem n 5)) "Buzz"
:else (str n)))
(defn print-fizzbuzz [n]
(doseq [i (range 1 (inc n))]
(println (fizzbuzz i))))
This looks overwhelming for someone who hasn't written any Lisp. My thought looking at this code was that I have to type a parenthesis before formulating any thought. Which is crazy because it's not some huge thing. It's like saying I have to press Enter before writing a line of Python.The lang itself is good and I recommend folks use a LISP sometime. I was just genuinely surprised a 17-year-old lang was lacking in these areas, and I'd be pretty careful about setting out on a long-term project with it, unless all of those things have radically improved from 6 months ago.
(And like other folks said, you genuinely need editor integrations to not be wasting all your time on pren balancing. Not clj's fault, just LISPs in general.)
A couple years with Rust has taught me that intuitive errors and tooling will funnel you far enough into language to get you productive, and then you're much more likely to stay. There's just no way I would have stayed long enough to be a Rust professional if it hadn't been for cargo and rust-analyzer.
These "non-language" components of Clojure are just not easy enough to use, and its inhibited Clojure's growth. If, however, you do put in the time to grok these parts, the joy of using the language itself never fades.
https://github.com/clojure/clojure/blob/master/src/jvm/cloju...
But like, where is the javadoc? What exactly is supposed to be the contract of these methods `first`, `next`, `more`, `cons`? What's the difference between `next` and `more`?
I really just don't like that. Are we just supposed to pick up the core contracts/abstractions through oral teachings and slack channel messages?
Tooling is a combo of Shadow CLJS and Clojure CLI.
[0] See `resources/public/fonts` here: https://github.com/eliascotto/tryclojure/tree/main/resources...
Compared to Go, it has a weak stdlib, is more memory intensive, and generally slower. You get beautifully concise code but it can be very hard to follow (or return to after time away).
Go can look completely idiotic or unbelievably focused and practical, depending on the light. It is painful to give up Clojure’s very strong selling points but I find alternatives to be more pragmatic.
"it can be very hard to follow": I agree, if you are not a lisp developer it's really hard, but if you develop clojure everyday, it's as easy as anything else.
The answer is usually clean code when it comes to clojure. Keep your functions small.
"more memory intensive, and generally slower": yep, the JVM is more memory intensive than Go. No surprise there and Clojure adds up on top of that. Startup times are pretty slow too. But compared to python it's still fast. Apples to oranges.
I would say Clojure and Go are both great languages that tackle different problems so it's not a fair comparison.
Regarding difficulty reading code, I'm not talking about syntax, I'm talking about the fact that many libraries define their own DSLs and you _cannot understand what they are doing_ by reading the code, you have to dig into the library internals to make sense of how they actually work, particularly when documentation is lacking or outdated.
Many problems can be solved by either language, and I wish it was easier to justify reaching for Clojure.
The contrast ratio here is 3.9:1 (text #DC2626, background #E5E7EB), and the minimum recommended for small text is 4.5:1.
Sorry to be that guy :) I am enjoying the tutorial.
1. When I read about Clojure (repeatedly) I like it. I am especially interested in transducers. I even built a half baked transducer engine in JavaScript using generators. There is clearly something here of value here. Any system that can produce transducers is worth learning.
2. I spent too many years of my life on Java. It was okay until someone wrote a thing in php (!!) in a day that would be have taken at least a month in Java. Maybe two. I understand the "good" of Java, but for me personally, the cost of that good is just incredible tedium. This is personal and I understand that.
3. However, as soon as I get to the part of installing Clojure where you install a JVM, I start hearing voices that say "It ain't me babe" and "Just say no to Java". PTSD?
For a while there was Clojure-script and I started working with that, but it seemed to fall off the edge of the world.
I will happily try Clojure again if it will compile itself or if it does not require the installation of a JVM. [ And last but not least of irrational reactions- anything associated with Oracle is to be avoided like the plague. sorry]
[Edit: then I saw the stuff about Babashka so I will give that a try]
I've been using Clojure heavily for the last 9 years or so and I can't see any reasons to dislike the JVM. Also, I barely ever touch any Java. You don't need to.
This means that you really don't need Java in order to use Clojure.
Coming from a C++ background, I used to dislike the JVM out of principle just like yourself, especially since it needs to run on my local machine and it uses so much memory and is slow to start up.
However, once your app is started, you're in the REPL and that's the only time you need to start your app.. You can keep developing for days without restarting your app once.
Once I finished developing my app and I deploy it to a server, I'm kind of happy it runs on the JVM - that thing is super tuned, very fast and runs on a myriad of hardware platforms.. I don't have to spend one minute thinking about those details.
So while the JVM is somewhat inconvenient on the dev machine, it helps a lot when you deploy it to production.
Things became a lot simpler when I stopped worrying about it and just used the language for its power and beauty.
I’ve done this with internal libraries - it’s easy to get them pulled into a Clojure code base, wrap them in such a way that the ergonomics of the rest of your code aren’t ruined, and still treat them as a first class citizen for stuff like your build system, artifact stores, etc.
I would urge you to push thru the PTSD and give it another shot.
There is also Clojurescript, which runs on Javascript, and Babashka which is a lighter weight implementation of Clojure for fast startup times that is targeted at things like shell scripts or system programs. https://babashka.org/
CLR: https://github.com/clojure/clojure-clr
LLVM: https://jank-lang.org/
Erlang: https://www.clojerl.org/
Python: http://hylang.org/
You understand that this has everything to do with Java and nothing to do with the JVM, right?
Really curious to hear what that was, because I'm having a hard time believing that is true.
As is it basically just shows you how to do arithmetic, which isn't interesting at all.
- Dependency management: https://clojure.org/guides/deps_and_cli
- Clojure->JS compiler: https://github.com/thheller/shadow-cljs
- React integration: https://github.com/reagent-project/reagent
- Global state management (optional): https://github.com/day8/re-frame
You might also want a CSS framework. There are some options to write CSS in ClojureScript, but I prefer TailwindCSS which isn't a Clojure-specific thing and it works fine out-of-the-box with `.cljs` files.
You can swap out Reagent for Helix, which is a lower-level and faster wrapper for React. That said, Reagent does work with React 18 just fine and there's tons of docs for it, so jumping to Helix first is a premature optimization IMO, especially if you're new to Clojure.
> "3(1 2)"
Yup, I'm out.
If you try that in Clojure, you'll get:
> ClassCastException class clojure.lang.PersistentList cannot be cast to class java.lang.Number
If you try it in a "proper" ClojureScript dev environment (shadow-cljs), you get nil as a result with this warning:
> cljs.core/+, all arguments must be numbers, got [number cljs.core/IList] instead
This is one of the real problems of clojure: you need some knowledge of the host language. It's also one of it's major strengths, and the only reason a lisp managed to get so much (relative) commercial traction.
(reference to: https://www.destroyallsoftware.com/talks/wat)
> (type (+ 1 2 '(1 2)))
#object[String]The same under Clojure (on top of the JVM) shall throw a class cast exception.
If you need method autocomplete scoped to a type you can use the `..` macro for more concise call syntax:
(ns my-project.core
(:import (java.util ArrayList Collections)))
(defn example []
(let [list (ArrayList.)] ; ArrayList constructor can be autocompleted
(.. list
(add "Hello") ; The .add method on ArrayLists will be autocompleted
(add "World")
(add "Clojure"))
(Collections/sort list) ; The Collections.sort method will be autocompleted
list))
this other form will a also autocomplete everything after the dot (.) to call a method on an object: (.toUpperCase some-str)
but the trick with this arrangement is to write that variable some-str first, then if you write . in front of it the autocompletes will be relevant to that object. But I gratuitously used threading macros like .. or -> to make it match java-style code (in other words: (-> some-str (.toUpperCase))).For JVM interop, I think Cursive (IntelliJ Clojure plugin) is smart enough to help with autocompleting Java libraries.
I can't believe that after all these years Java still didn't fix their startup time.
[1]: E.g. see https://spring.io/blog/2023/10/16/runtime-efficiency-with-sp...
Im yet to reach the X-men level super qualities that can detect, and work in 40 ms chunks. Or at least even notice a 40 ms delays.
I envy the humans who can notice such small chunks of time.
Isn't the initialization procedure (or at least the vast majority of it) exactly the same at each run ?
That said, there are alternative runtimes that have different tradeoffs. For example, Babashka is a runtime for Clojure that uses GraalVM instead of the JVM as the foundation. Babashka scripts have about a 10ms startup time on my M1 MacBook Air.
In some sense it's Clojure's fault for having an implementation that causes slow JVM startup, but it's also the JVM's fault that the way Clojure uses it causes it to take a long time to start.
But before Babashka, that was indeed a barrier to using Clojure in shell scripts. Now we have it all!
I feel a good fraction of code will dip into Java libs at least a bit - so you're limited in what libraries you can use
I think the real solution is probably Graal native - though it's not part of the official toolbox/deps.edn
It's also possible to compile your JVM Clojure program yourself to a binary with GraalVM for even better performance than Babashka and even faster startup.
Much more rarely — mostly when talking about writing CLI tools in a JVM language — people actually are complaining about the single second-or-so it takes the JVM to start up. (Usually because they want to run this tool in a loop under xargs(1) or find(1) or something.)
This last second of startup lag is (AFAIK) quite hard to improve, as it's mostly not the JVM itself starting up, but the static methods of JVM classes being called as those classes are loaded — which can do arbitrarily much stuff before any of your own code gets control. (Due to legacy code expecting to read certain per-boot-dynamic info as static fields rather than as the results of static method calls, I believe the JRE runtime is actually required to do quite a lot of that kind of static initialization, to pre-populate all those static fields, just in case something wants to read them.)
---
You'd think that GraalVM could inherently skip most of this, because the Graal compiler does dead-code analysis. "If nothing in your code reads one of those static fields, then nothing needs to write that field, so let's not invoke the static initializer for that field." But that's not true: static initializers are exported and called by the runtime — so they're always "alive" from the compiler's perspective. The Graal compiler isn't doing full-bore data-flow analysis to determine which static members of which classes are ever read from.
I believe GraalVM does try to work around static initializers as much as it can, by pre-evaluating and snapshotting as much of JVM runtime's static initializer code as possible by default, converting it all into const data structures embedded in the class files before the native codegen step gets run on it (see: https://www.graalvm.org/latest/reference-manual/native-image...).
It's not possible to apply this pre-baking to 100% of classes, sadly — some of these data structures need environment data like env-vars or system network config threaded into them on boot.
(I wonder anyone on the Graal project is working on a fully-general static-initializer optimization pass, that does something like concolic execution to bake these initializers out into either fullly-constant data if possible, or if not, then constant "data templates" plus trivial initializer functions that just gather up the few at-boot context fragments, shove them into the "data template" using a low-level hook, and then memcpy the result out onto the heap and call it an object.)
It's how much code you load.
E.g. any Python project has to deal with this, like Mercurial.
Java projects tend to (insert stereotype) have a lot of code.
No operator precedence rules to remember.
Use math operators in higher order functions:
(reduce [1 2 3] +)
(compose + \*)
Simpler parser in the language.Use kebab-case variable names.
Use most symbols in variable names.
The last two are because of reduced ambiguity in the language. The benefits for readability can be huge. For example, I love question marks for predicates in Scheme.
(vector? x)
(filter [1 2 3 4 5] odd?)
I'm sure there are more benefits that I haven't thought of.To be fair, there just needs to be syntax for passing infix functions as a "normal" function, like parens in Haskell
(reduce [1 2 3] +)
foldl' (+) 0 [1, 2, 3]
And to allow `?` in names, it "just" shall not be used elsewhere.As an added bonus, you now know Lisp. This is how all Lisp syntax works: `(function arg1 arg2 ...)`. That's literally it.
The main one being if you want to add many many items together it's much easier IE (+ 1 2 3 4 5 6).
Instead of doing an operation between two numbers you are apply a function to a list of inputs. In the "+" function's case you are adding them together. With this in mind, it's no different than most other programming languages. You would never have your function be in the middle of your arguments/inputs.
In python its Foo(bar, bar) and not bar Foo() bar, which obviously doesn't make sense.
The real advantage of this notation is that it's much, much easier to stack calculations.
Example:
(/ (+ 34 68 12 9 20) 140)
You can imagine how the first part of that could come about:
(+ 34 68 12 9 20)
And then the second part (pseudocode):
(/ sum 140)
In Clojure it's easy to mash them together, or for example to paste the first calculation in the second.
(/ (+ 34 68 12 9 20) 140)
Want to go further? Easy: add another enclosing parens.
(* (/ (+ 34 68 12 9 20) 140) 1.5 2)
Note how we're stacking in a more human-readable order - a new calculation starts on the left hand side, the first thing we see as we read it.
Compare how verbose the alternative is:
((34 + 68 + 12 + 9 + 20) / 140) * 1.5 * 2
The problem with that is by the time I reach `20)` I have already forgotten what operation I'm in. I'd write it more like this:
(*
(/ (+ 34 68 12 9 20)
140)
1.5 2)
actually I'd write this as (/
(* 2 (/ 3 2) (+ 34 68 12 9 20))
140)
> ((34 + 68 + 12 + 9 + 20) / 140) * 1.5 * 2Why not (34 + 68 + 12 + 9 + 20) * 1.5 * 2 / 140?
If you work with clojure a lot, does it become natural?
Where it really shines though is when you use the threading macro and inline comments to make really complex math a breeze to review:
(-> (+ 34 68 12 9 20) ;; sum all the numbers
(/ 140) ;; divide by a quotient for some reason
(* 2) ;; multiply by two for it's own reason99% of the syntax is just (foo arg1 arg2).
(-> (+ 34 68 12 9 20) (/ 140))
mv foo.txt bar.txt # rename a file in the Unix shell
Except there are parentheses to delimit the command because commands can be nested in each other.(Some Lisps have interactive modes where you can drop the outermost parentheses, allowing you to type like this:
prompt> + 1 1
2
but usually the parentheses are part of the formal syntax; where this is just a hack in the interactive listener.)The design choice came about because the syntax started as an internal representation for a symbolic processing (formula manipulation) system that was being designed by John MacCarthy. The internal representation where the operator is first followed by its arguments was convenient because you don't have to parse around to identify them. You know immediately that by accessing the first position of the formula, there is going to be a symbol there which identifies its operator.
The internal form, and its written notation came to be used directly, while the symbol manipulation system came to be programmed in itself, so that its own "formulas" (source code) ended up in that form.
I know it's really hard to see how. (And honestly it's sometimes a little hard for me to see what the big deal is, but one of my first programming languages was a dialect of lisp so I can barely even remember not being comfortable with them.) But many people find that, once they get used to them, and especially if they learn to use an editor with a paredit mode, they can start to feel even easier to work with than infix syntax.
That allows for some nifty tricks like:
(reduce + [1 2 3]) ; returns 6
(In my functional programming languages, `reduce` also requires passing an initial value, like `foldl (+) 0 [1, 2, 3]`. But surely it should get a monoid instead of two separate arguments for the operation and the starting value!)
(some-function-name arg1 arg2)
Personally I don't feel this strong urge to tell communities that I'm not interested in them. Waste of my time and theirs.
If you need to do graphql in typescript, you still "learning a new language". If you're using Prisma or you doing css-in-js, or rxjs, or tRPC, or Cypress, you're still "learning a new language".
You're basically moving from having to understand one (seemingly arbitrarily composed) syntax to another (seemingly arbitrarily composed) syntax. While I learned Clojure syntax once, and now only have to learn paradigms and techniques - logic, pattern-matching, concurrency, reactive dataflow, polymorphic dispatch, metaprogramming, event-driven programming, state management, etc. etc.
My hope is that I never have to use TypeScript, Go, or Rust daily since Clojure already gives me everything I need to write programs. It simply makes much better sense for the type of programs I'm writing today. Someday that may change, but I doubt that any of those would suddenly start making better sense to use. It probably will be something else, not these.
Don't feel like being the Blub programmer and missing out on new good stuff.
> If you want everything to be familiar, you'll never learn anything new. - Rich Hickey
My hot take -- There are already too many programming languages in use and every engineers life would be easier and productivity would be higher if we standardized.
There is almost no situation where making an entirely new language is the optimal solution. And the worst reason of all to make a language is anything aesthetic (e.g. spacing, parentheses, notations) -- all of those cases should be handled via native transpiling in the IDE.
There are some good examples of applications that are specifically written in Clojure and would be quite difficult to replicate in other PLs - Roam Research and Logseq, XTDB and Datomic, Nextjournal and Precursor.
> And the worst reason of all to make a language is anything aesthetic
Interestingly, people avoid learning Lisp, because it doesn't look "sexy enough" the first time they see it.
Every year dozens of people think they're going to make some holy-grail language and it's 1% different from the prior version but now every single library needs to be rewritten, a dozen years of patches/bugs/security-holes must happen, whole resumes get shined up rewriting tech stacks that were perfectly capable, all for it to be thrown for the next shiny toy in less than a decade.
A new language just fractures knowledge.