clojure

imre 2026-02-26T11:04:10.910049Z

Is there a commonly used name for the pattern when you use a let around a defn?

imre 2026-02-27T09:28:57.085139Z

> Isn't it tidier to use def at the top level only @phill I don't have an issue with let over defn. defn communicates well that it's a function, and the let over it lets me extract stuff that only needs to be initialized once. Plus, as @p-himik pointed out, defn adds additional metadata that wouldn't be there when using (def foo (fn ))

Edward Hughes 2026-02-27T10:40:46.226249Z

I'm not sure I see any meaningful benefit, semantic or otherwise, in defining non-local state inside a closure regardless of whether it is a function or any other value. It kinda defeats the encapsulation purposes of a closure if I can reference things inside it by name from the namespace scope. Defn and def are globally scoped, not lexicallly. That's a significant difference in behaviour from a let-over-lambda. Maybe inline wasn't the right word, but def and defn declarations have no real reason to be anything other than top level forms, entirely because they are side-effectful changes to the environment. At least that's the best interpretation for why there is a hard rule against doing otherwise in the style guide. I've actually seen nested defns enough times in the wild that I am tempted to categorise it as one of the more common syntactic antipatterns.

imre 2026-02-27T12:13:15.355559Z

As far as I know, the following implementations

(ns my.uber.ns)

(let [threshold-implementation-detail (expensively-calculate-threshold)]
  (defn enough-a? [x]
    (< threshold-implementation-detail x)))

(defn enough-b? [x]
  (let [threshold-implementation-detail (expensively-calculate-threshold)]
    (< threshold-implementation-detail x)))
differ in that (expensively-calculate-threshold) will only be called once for the first one, but will be called on every invocation of the function in the latter @edward.hughes1911 do you consider the former an anti-pattern?

p-himik 2026-02-27T12:20:04.817759Z

> It kinda defeats the encapsulation purposes of a closure if I can reference things inside it by name from the namespace scope. You can never reference things "inside a closure". You can only reference things that are defined/passed elsewhere, and those things might happen to also be closed over by something. > def and defn declarations have no real reason to be anything other than top level forms There are reasons. Dynamic code, conditional defs, and incapsulation that you mentioned yourself. > that's the best interpretation for why there is a hard rule against doing otherwise in the style guide The style guide is a documented opinion of a few people that sometimes don't even agree with each other. :) It's not the guide that everyone must abide by. That's not to say that doing defs inside some function where a let would work is a good idea anywhere except some REPL-friendly dev-time shenanigans.

➕ 1
p-himik 2026-02-27T12:35:00.999869Z

A practical example from clojure.core:

(let [^java.util.Properties
      properties (with-open [version-stream (.getResourceAsStream
                                             (clojure.lang.RT/baseLoader)
                                             "clojure/version.properties")]
                   (doto (new java.util.Properties)
                     (.load version-stream)))
      version-string (.getProperty properties "version")
      [_ major minor incremental qualifier snapshot]
      (re-matches
       #"(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9_]+))?(?:-(SNAPSHOT))?"
       version-string)
      clojure-version {:major       (Integer/valueOf ^String major)
                       :minor       (Integer/valueOf ^String minor)
                       :incremental (Integer/valueOf ^String incremental)
                       :qualifier   (if (= qualifier "SNAPSHOT") nil qualifier)}]
  (def ^:dynamic *clojure-version*
    (if (.contains version-string "SNAPSHOT")
      (clojure.lang.RT/assoc clojure-version :interim true)
      clojure-version)))

➕ 1
henrik 2026-02-27T14:44:39.371879Z

(letfn [(impl [] true)]
  (def is-this-the-correct-way-to-declare-a-function? impl))

🤕 4
practicalli-johnny 2026-03-06T08:57:37.800089Z

I would say a "let wrapping a defn" is a code smell for Clojure. Giving this approach a name could easily make it more of a default choice. The Coursera Clojure course has lots of examples of nested defn forms, easily misleading learners into thinking its the default approach. To me it is adding complexity, especially when understanding scope. If there is justafiable case for this approach for in a particular scenario, it should be clearly documented as to why (I've seen many engineers rerwrite code or namespaces because code was obscure) There are several code edge cases in Clojure.core that were used to make a programming language. Those edge cases may notbe the most effective design for your application code (and its maintence). The harder we make Clojure code bases to work with, the less likely new people will want to work with Clojure.

➕ 2
imre 2026-03-06T13:46:00.898759Z

I think that's too harsh. Going from

(defn enough? [x]
  (< (expensively-calculate-threshold) x))
to
(let [threshold (expensively-calculate-threshold)]
  (defn enough? [x]
    (< threshold x)))
is IMO idiomatic when you only want to extract something for, say, performance concerns but what you extract is still semantically an implementation detail tightly coupled to the function being defined.

👎 1
➕ 3
imre 2026-02-26T11:04:29.777069Z

like

(ns my.uber.ns)

(let [threshold-implementation-detail (expensively-calculate-threshold)]
  (defn enough? [x]
    (< threshold-implementation-detail x)))

p-himik 2026-02-26T11:05:40.801189Z

Never seen a name for it.

2026-02-26T11:09:47.020679Z

Isn't it just a closure? The enough? function "closes over" threshold.

➕ 5
imre 2026-02-26T11:10:22.490489Z

It is a kind of closure yes.

p-himik 2026-02-26T11:10:39.163869Z

Closures can come from anywhere and don't necessarily mean that there's a let right outside defn.

2026-02-26T11:11:11.417039Z

I'm not aware of a more specific term for using defn like this.

imre 2026-02-26T11:15:41.246929Z

I tend to use let-around-defn or let-over-defn which make sense to me - probably because I know and use the pattern. What I'm hoping is that there is a phrase one can put into google and arrive at a guide that explains it so I don't have to every time 😛

p-himik 2026-02-26T11:17:19.086989Z

> arrive at a guide that explains it But what is there to explain? let exists, defn also, and you can use them together - like a plethora of other forms.

teodorlu 2026-02-26T11:24:50.884909Z

It's almost let over lamda. https://letoverlambda.com/

➕ 4
☝️ 1
😄 4
imre 2026-02-26T11:26:43.547669Z

> you can use them together - like a plethora of other forms it's just that you encounter defn s as top level forms most of the time and devs newer to the language (and lisps in general) might not be aware that this is something you can do - like how it was a revelation to me when I learned about it

p-himik 2026-02-26T11:33:46.683659Z

Perhaps, although... Can't speak for others of course, but it feels like a vestige from other languages that a person might've learned prior to Clojure. As such, there's an endless amount of such vestiges. "Functions are values?! Arg vectors are... vectors?! You can just print something?! 🤯" And so on.

imre 2026-02-26T11:53:38.940429Z

I'm not trying to be exhaustive here 🙂

imre 2026-02-26T11:54:03.474209Z

And there are already good docs related to the examples you brought

imre 2026-02-26T11:54:35.411039Z

I guess I can just add an example to clojuredocs and keep linking to it 😛

p-himik 2026-02-26T11:55:40.605149Z

I'd advise against making it that narrow though. IMO "let-over-defn" makes much less sense in the context where defn can appear pretty much anywhere. You can even (let [x (defn ...)] ...).

imre 2026-02-26T11:58:57.044719Z

I'll let others document those if they think it's worth it. Right now I'm just looking for some referable documentation of the pattern I mentioned above

2026-02-26T13:21:19.283179Z

that's a let over lambda, aka a closure

🎯 3
2026-02-26T13:22:59.706549Z

defn is "just" (def foo (fn ...)), and clojure's var definition semantics means there's no difference between (let [] (def foo ...)) and (def foo (let [] ...)), so you could feasibly achieve the same thing with (def foo (let [] (fn [] ...)))

➕ 1
2026-02-26T13:25:27.879279Z

the doug hoyte book @teodorlu mentioned above is a dive into the concept using common lisp (with some cool macros and a classic "smug lisp weenie" voice)

2026-02-26T13:27:47.926119Z

the reason you might do such a thing in clojure is to fully hide the variable being closed over. if it's in a ^:private var, it can still be modified or redef'd or unmapped, but a closure is opaque and barring ASM nonsense the variable can't be touched

Edward Hughes 2026-02-26T13:32:26.997279Z

In terms of style, an inline def or defn is generally considered bad form. As mentioned above, this is basically a first-order let-over-lambda, but a better way to represent it is with

(let [my-local-fn (fn [x] ...)])
or a let-fn https://clojuredocs.org/clojure.core/letfn

Edward Hughes 2026-02-26T13:40:35.431009Z

Cf. https://guide.clojure.style/#dont-def-vars-inside-fns

👆 1
imre 2026-02-26T14:24:47.704869Z

"let over lambda" or "let over defn" are the top contenders so far 🙂

imre 2026-02-26T14:26:05.744569Z

I'm not suggesting inline defs btw, in case anyone believed that

imre 2026-02-26T14:26:12.305859Z

let me update the example

2026-02-26T23:27:18.491209Z

Isn't it tidier to use def at the top level only: in other words, to prefer (def foo (let [...] (fn ...

2026-02-26T23:27:40.637359Z

Hoyte's book is a hoot. In a field of texts that are workmanlike at best, "Let over lambda" is a work of art.

p-himik 2026-02-26T23:29:34.684969Z

FWIW I don't think it's tidier. But it's also not the same. At the very least, the metadata would be different.

2026-02-26T23:30:05.349999Z

lol if you go in knowing he's going to shit talk all non Common Lisp lisps, it's got some great stuff. but he's an abrasive writer

2026-02-26T23:43:24.031099Z

"Let over lambda" is a joy no matter how you read it. Either it is an epic-length Monty Python send-up of lunatic-fringe Lisp-2 under the guise of a defense of it (in which case it's fun) or it is dead serious (in which case you feel better and better about using a Lisp-1 until you reach max delight and toss the book aside after the third or fourth chapter). Or a bit of both. It's proof you can write a heck of a book about a hell of a system.

➕ 1
Edward Hughes 2026-03-27T14:06:28.789349Z

Unfortunately, putting top level forms inside other forms is simply not idiomatic Clojure, as documented by multiple sources provided. You can put ns declarations inside a function and have it compile, but there's a reason we don't do that.

imre 2026-03-27T14:08:19.698269Z

Which source says that?

➕ 1
imre 2026-03-27T14:09:14.714629Z

(something being a "top-level form" is a result of placement of a form in a file, not what the head symbol is)

Edward Hughes 2026-03-27T14:18:38.011509Z

>top level form is a relative property resulting from the location of a form in a file Incorrect. ns, def, defn, defmacro etc. are all top-level forms specifically intended for use at the top level of an s-exp only. The existence of letfn and similar macros are specifically meant to address cases where you want to achieve the same result as top-level forms inside a closure. Please refer to the style guide I linked last month. There are multiple other sources and discussions on the first page of google results, including the core language docs themselves that concur with this.

imre 2026-03-27T14:22:48.233429Z

1. the screenshot doesn't support your claim that there are forms that are top level by definition 2. it talks about modifying the root value of a var which my example does not do 3. the https://guide.clojure.style/#dont-def-vars-inside-fns above discusses def-ing something inside a function which my example also doesn't do

👍 1
Edward Hughes 2026-03-27T14:50:09.233559Z

I fear you are missing the forest for the trees. The initial binding of a value, function body or otherwise, to a var is by definition modifying the value of that symbol from nil to some value. You modified the root value of the var by declaring it. It's not a huge leap of logic to go from "deffing vars inside functions is bad" to "deffing vars in closures, that are part of function implementations is bad". Transitive property and all. I assume your let isn't going to just sit in some namespace dangling by itself to be run every time the ns is loaded. For the purposes of structural editing and repl eval, top-level is relative. For the purposes of certain special forms like the one mentioned, being top-level is part of their intended use. You can ask the people who actually hashed out these patterns while architecting the language, but I would prefer not to waste their time on something so frivolous and readily apparent. It is a bit odd to me to be making declarations of what can be considered idiomatic without any sourcing, when a cursory search would return an overwhelming number of posts where people discuss def and defn and reference them as inherently top-level. Just because other languages and other lisps don't apply that distinction doesn't mean you should do it in Clojure. https://stackoverflow.com/questions/23255798/clojure-style-defn-vs-letfn#:~:text=Definitely%20nothing%20wrong%20with%20using,6466%209

2026-03-27T14:53:16.368929Z

i'm not a mod, but these replies feel hostile, edward. we're all discussing our opinions in this thread. there's no right way to do it, and even clojure core has def forms that close over a let binding (`*clojure-version*`, as highlighted in this thread). i understand that you feel strongly about it. may i suggest couching your thoughts with "i think" or "this is how i've interpreted it" or "here's the way i've thought of it"

☮️ 1
imre 2026-03-27T14:59:01.499439Z

> you are missing the forest for the trees hmmm > deffing vars in closures, that are part of function implementations is bad again, that isn't what my example is doing > your let ... sit in some namespace dangling by itself to be run every time the ns is loaded this is precisely what this thread is about

imre 2026-03-27T15:08:43.478519Z

> being top-level is part of their intended use I've still yet to see an authoritative source saying that. The link Noah just posted actually demonstrates this very technique in clojure.core > I would prefer not to waste their time Same, that's why I'm asking the community here, and I welcome your participation > something so frivolous and readily apparent Again, this claim could use some backing up. Before I asked here I did search around and could not find any sources that seriously discourage this practice. > posts where people discuss def and defn and reference them as inherently top-level I consider the people of the Clojurians slack community a better source of information on these questions than random stack overflow tbh.

henrik 2026-03-27T15:21:40.778929Z

I ~recently saw a thread on HN where someone argued that recursion wasn’t immutable enough because the binding gets redefined in each loop/call.

😅 2
henrik 2026-03-27T15:22:57.067299Z

We can draw the line in different places, is my point.

henrik 2026-03-27T15:23:33.950899Z

Thank the Powers that Be that we have a language that lets us.

Edward Hughes 2026-03-27T16:48:13.865289Z

clojure-version feels like an exception that proves the rule. It's a 17 year old var, the only example of a closed over def in the core outside of 3 macro definitions where consing up code forms to get specific behaviour out of the runtime is the point, and there's no apparent reason to me why you couldn't convolute the forms so that the let is inside the def instead. I assume Michiel left it that way as a matter of commit parsimony when removing the reflection and one style change on a dynamic var in a 8k loc file isn't worth the time to go back and fix. If I sound frustrated, it's because I have cited multiple sources to support my statements of opinion and fact, and practicalli, who has published a significant amount of training material, has also chimed in to the same effect, but the conversation hasn't moved in a meaningful direction in a month and it feels like we are talking past each other for the sake of talking. Saying "i think this is not a code smell worth worrying about" is a statement of opinion. Saying "this is idiomatic clojure" is making a factual claim that can be shown to be true or false by observation of the wider context. And frankly, right now I feel like making a good faith effort to verify that truth claim is a waste of my time when I am trying to work to keep a roof over my head. At the end of the day, I have bigger things to worry about than conclusively proving with citations the existence of an idiom that has been generally documented and is a logical consequent of the wider discourse and widely accepted reference material like the style guide and language docs. I could have gone into a lot more detail in my responses to the points raised in this thread, but I felt like that would have been covering well-trod ground and irrelevant to demonstrating that both from a descriptivist (as used by the preponderance of the language community) and prescriptivist (as defined by authoritative sources), using defs and defns that way is inadvisable and unidiomatic for common functions or code invocations. I personally do not care if you choose to write your code that way, libertarian programming means you get to do what you want, even inadvisable things. I would have reservations working on a project where such a pattern was the norm outside of very specific usage in a defmacro (that has its own set of idioms) since it breaks FP principles and the general advice of the community. I care immensely if someone asks for my opinion or experience and then keeps coming up with reasons to ignore them out of thin air despite considering the forum in which I voice them a source of authority. >stackoverflow is less authoritative than slack SO used to be my first port of call to find Alex's, Rich's, Sean's, Stuart's, et ceterorum opinion on something. I certainly wouldn't rule it out as a source of truth when you have multiple people of varying levels of notability saying similar things over a decade or so. Especially if we are discussing what can be considered idiomatic or not. Take it or leave it, I don't think there is any more I can say to make my position or observations clear.

imre 2026-03-27T17:00:51.186269Z

> there's no apparent reason to me why you couldn't convolute the forms so that the let is inside the def instead One reason could be https://clojurians.slack.com/archives/C03S1KBA2/p1772804760898759?thread_ts=1772103850.910049&amp;cid=C03S1KBA2: if the let is inside the defn, the expensive operation runs every time the function is invoked. If the let is outside, it runs only once, when the top-level form (in this case, the let) is evaluated, for example upon loading the namespace > I have cited multiple sources to support my statements of opinion and fact True, it's just that most of those deal with a practice different from what I brought up in this thread and are therefore irrelevant, and one SO post where someone I don't know to be close to the Core team said something similar to what you are saying but also without backing that up. > practicalli I appreciate his chiming in and would love to read his response to what I responded to his comment. Thanks for your time. I'd still be interested to learn why you think it breaks FP principles

p-himik 2026-03-27T18:06:45.366089Z

> there's no apparent reason to me why you couldn't convolute the forms so that the let is inside the def instead So suppose I have some (let [...] (defn ...)), how would you rewrite it so that it's in the form of (def (let [...] ...)? The naive direct approach doesn't work properly, the less direct approach with manually assigned metadata is prone to errors, cumbersome, and of limited usefulness when it comes to IDEs. There's simply no alternative to (let [...] (defn ...)) that would be a full analog without severe downsides and with no upsides apart from the still quite unclear "`def` inside let is bad". As for "idiomatic Clojure", I'd say it's quite subjective simply because there are no clear metrics. But one could look at everything that's under org.clojure/..., not just inside Clojure itself. After all, let over defn is useful in specific contexts that might not be relevant to the standard library of a language. I'd also say that let over defmethod/`defmulti`/`defmacro`/various extend calls counts since those are quite similar to defn. So, files that use let over defn and friends (removed some entries that are about testing whether let-over-def works at all or that were clearly ported between platforms; multiple defn inside a single let were counted as separate entries): • algo.generic/src/test/clojure/clojure/algo/generic/test_complex.clj (1 time) • clojurescript/src/test/cljs/cljs/core_test.cljs (3 times) • clojure/test/clojure/test_clojure/reader.cljc (1 time) • clojure/test/clojure/test_helper.clj (1 time) • core.match/src/main/clojure/clojure/core/match.clj (1 time) • core.rrb-vector/src/main/clojure/clojure/core/rrb_vector/debug.clj (2 times) • core.typed/typed/runtime.jvm/src/clojure/core/typed.clj (36 times) • core.typed.runtime.jvm/src/main/clojure/clojure/core/typed/macros.clj (6 times) • core.typed.checker.js/src/main/clojure/clojure/core/typed/analyze_cljs.clj (2 times) • core.typed.checker.js/src/main/clojure/clojure/core/typed/base_env_cljs.clj (4 times) • core.typed.checker.jvm/src/main/clojure/clojure/core/typed/checker/jvm/base_env.clj (2 times) • core.typed.checker.jvm/src/main/clojure/clojure/core/typed/checker/jvm/parse_unparse.clj (1 time) • core.typed.checker.jvm/src/main/clojure/clojure/core/typed/checker/jvm/subtype.clj (2 times) • core.typed.checker.jvm/src/main/clojure/clojure/core/typed/checker/type_ctors.clj (7 times) • core.typed.runtime.jvm/src/main/clojure/cljs/core/typed.clj (6 times) • core.typed.runtime.jvm/src/main/clojure/clojure/core/typed/all_envs.cljc (2 times) • core.typed.runtime.jvm/src/main/clojure/clojure/core/typed/ast_utils.clj (2 times) • core.typed.runtime.jvm/src/main/clojure/clojure/core/typed/errors.cljc (2 times) • core.typed.runtime.jvm/src/main/clojure/clojure/core/typed/load.cljc (7 times) • core.typed.runtime.jvm/src/main/clojure/clojure/core/typed/parse_ast.cljc (1 time) • data.finger-tree/src/main/clojure/clojure/data/finger_tree.clj (3 times) • data.xml/src/main/clojure/clojure/data/xml/event.clj (2 times) • spec-alpha2/src/main/clojure/clojure/alpha/spec/gen.clj (3 times) • spec.alpha/src/main/clojure/clojure/spec/gen/alpha.clj (3 times) • test.check/src/main/clojure/clojure/test/check/generators.cljc (1 time) • tools.logging/src/main/clojure/clojure/tools/logging.clj (2 times) • tools.logging/src/test/clojure/clojure/tools/logging/test_impl.clj (2 times) Haven't measured but I bet it's more entries than agent and STM usages under org.clojure combined.

👍 2
imre 2026-03-27T18:28:05.870439Z

Nice research @p-himik! How were you able to pull that data?

p-himik 2026-03-27T18:31:35.832279Z

Just good old elbow grease.

👍 1
yannvahalewyn 2026-03-01T11:58:08.295759Z

This is the way

2026-02-26T15:31:14.757889Z

Which llm is a good choice for coding clojure? I can run 8b locally easily...

Samuel Ludwig 2026-02-26T15:33:24.491899Z

#ai-assisted-coding might be a better place to ask

seancorfield 2026-02-26T15:35:59.052429Z

Also, please use threads to provide more detail to an original question, instead of posting multiple messages in the channel. Thanks.

1
seancorfield 2026-02-26T15:37:11.685329Z

"I use opencode and ollama if that is helpful information.." -- reposted your follow-up here (and deleted the original), so that any other replies come to this thread (or the #ai-assisted-coding channel if you ask there).