clojure

Ertugrul Cetin 2026-03-26T08:20:05.246379Z

What is the difference at the JVM bytecode level between these two calls? Is there any performance difference, especially in hot-paths?

(.glClearColor gl (float 0) (float 0) (float 0) (float 0))
vs
(.glClearColor gl 0 0 0 0)

2026-03-26T08:46:39.368579Z

Maybe https://github.com/clojure-goes-fast/clj-java-decompiler Can help you answer this.

☝️ 1
p-himik 2026-03-26T09:02:52.250579Z

And you can do it without any additional tools - just compile the code and view it with javap. As for the second question - any performance difference is very unlikely.

Ertugrul Cetin 2026-03-26T09:23:41.566039Z

Thanks for the reply, I'm going to use those tools 👍

Ertugrul Cetin 2026-03-26T09:46:55.656319Z

I used clj-java-decompiler and I got this results; Also got help from an LLM that summarizes like this;

With (float 24) — primitive path:
  ldc2_w 24                          // push long constant (8 bytes, on stack)
  invokestatic RT.floatCast:(J)F     // long→float, primitive-to-primitive
  That's it. No allocation, no indirection. RT.floatCast(long) is just a cast.

  Without — boxed path:
  // static init (once): box each literal into a Long object
  Long.valueOf(24) → stored in const__0 (Object field)

  // every invocation:
  getstatic const__0:Object          // load boxed Long from static field
  checkcast Number                   // runtime type check
  invokestatic RT.floatCast:(Object)F // unbox Number → doubleValue() → cast to float
(dd/disassemble
    (defn clear []
      (.glClearColor (Gdx/gl) (float 24) (float 25) (float 26) (float 27))))

// Decompiling class: api/canvas$clear
class api.canvas$clear
    Minor version: 0
    Major version: 52
    Flags: PUBLIC, FINAL, SUPER
    
    public void <init>();
        Flags: PUBLIC
        Code:
                  linenumber      2
               0: aload_0        
               1: invokespecial   clojure/lang/AFunction.<init>:()V
               4: return         
    
    public static java.lang.Object invokeStatic();
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      3
               0: getstatic       com/badlogic/gdx/Gdx.gl:Lcom/badlogic/gdx/graphics/GL20;
               3: checkcast       Lcom/badlogic/gdx/graphics/GL20;
               6: ldc2_w          24
                  linenumber      3
               9: invokestatic    clojure/lang/RT.floatCast:(J)F
              12: ldc2_w          25
                  linenumber      3
              15: invokestatic    clojure/lang/RT.floatCast:(J)F
              18: ldc2_w          26
                  linenumber      3
              21: invokestatic    clojure/lang/RT.floatCast:(J)F
              24: ldc2_w          27
                  linenumber      3
              27: invokestatic    clojure/lang/RT.floatCast:(J)F
                  linenumber      3
              30: invokeinterface com/badlogic/gdx/graphics/GL20.glClearColor:(FFFF)V
              35: aconst_null    
              36: areturn        
    
    public java.lang.Object invoke();
        Flags: PUBLIC
        Code:
                  linenumber      2
               0: invokestatic    api/canvas$clear.invokeStatic:()Ljava/lang/Object;
               3: areturn        
    
    static {};
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      2
               0: return         

=> nil
--
(dd/disassemble
    (defn clear []
      (.glClearColor (Gdx/gl) 24 25 26 27)))

// Decompiling class: api/canvas$clear
class api.canvas$clear
    Minor version: 0
    Major version: 52
    Flags: PUBLIC, FINAL, SUPER
    
    public static final java.lang.Object const__0;
        Flags: PUBLIC, STATIC, FINAL
    
    public static final java.lang.Object const__1;
        Flags: PUBLIC, STATIC, FINAL
    
    public static final java.lang.Object const__2;
        Flags: PUBLIC, STATIC, FINAL
    
    public static final java.lang.Object const__3;
        Flags: PUBLIC, STATIC, FINAL
    
    public void <init>();
        Flags: PUBLIC
        Code:
                  linenumber      2
               0: aload_0        
               1: invokespecial   clojure/lang/AFunction.<init>:()V
               4: return         
    
    public static java.lang.Object invokeStatic();
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      3
               0: getstatic       com/badlogic/gdx/Gdx.gl:Lcom/badlogic/gdx/graphics/GL20;
               3: checkcast       Lcom/badlogic/gdx/graphics/GL20;
               6: getstatic       api/canvas$clear.const__0:Ljava/lang/Object;
               9: checkcast       Ljava/lang/Number;
              12: invokestatic    clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
              15: getstatic       api/canvas$clear.const__1:Ljava/lang/Object;
              18: checkcast       Ljava/lang/Number;
              21: invokestatic    clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
              24: getstatic       api/canvas$clear.const__2:Ljava/lang/Object;
              27: checkcast       Ljava/lang/Number;
              30: invokestatic    clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
              33: getstatic       api/canvas$clear.const__3:Ljava/lang/Object;
              36: checkcast       Ljava/lang/Number;
              39: invokestatic    clojure/lang/RT.floatCast:(Ljava/lang/Object;)F
                  linenumber      3
              42: invokeinterface com/badlogic/gdx/graphics/GL20.glClearColor:(FFFF)V
              47: aconst_null    
              48: areturn        
    
    public java.lang.Object invoke();
        Flags: PUBLIC
        Code:
                  linenumber      2
               0: invokestatic    api/canvas$clear.invokeStatic:()Ljava/lang/Object;
               3: areturn        
    
    static {};
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      2
               0: ldc2_w          24
               3: invokestatic    java/lang/Long.valueOf:(J)Ljava/lang/Long;
               6: putstatic       api/canvas$clear.const__0:Ljava/lang/Object;
               9: ldc2_w          25
              12: invokestatic    java/lang/Long.valueOf:(J)Ljava/lang/Long;
              15: putstatic       api/canvas$clear.const__1:Ljava/lang/Object;
              18: ldc2_w          26
              21: invokestatic    java/lang/Long.valueOf:(J)Ljava/lang/Long;
              24: putstatic       api/canvas$clear.const__2:Ljava/lang/Object;
              27: ldc2_w          27
              30: invokestatic    java/lang/Long.valueOf:(J)Ljava/lang/Long;
              33: putstatic       api/canvas$clear.const__3:Ljava/lang/Object;
              36: return         

=> nil

👍🏼 1
Jan Šuráň 2026-03-26T15:43:18.065829Z

Hello, I recently asked a question on http://ask.clojure.org regarding defn not providing an implicit recursion point that has not been answered yet. Consider this example:

(defn fib [n]
  (case n
    0 0
    1 1
    (+ (fib (dec n))
       (fib (- n 2)))))
Here, the recursive call of fib function doesn't call the function directly, but rather goes through dereferencing of the var #'fib , which is a waste of runtime performance. Is there a reason for this? I believe Clojure could just implicitly create a named recursion point there, but it doesn't do that at the moment. Also, another similar question: Why doesn't Clojure optimize tail recursion for named functions, like in this example?
((fn foo [x]
   (when (pos? x)
     (foo (dec x))))
 1000000)
This code blows up on stack overflow, but I believe it should not be hard to internally replace this with recur if possible.

hrtmt brng 2026-03-30T19:12:01.438069Z

Maybe Clojure does this, because you could have rebound the function before you call it. The compiler cannot know this. Clojure calls are Java calls. That is a design decision. I guess all this is way more complicated than we all think. And maybe it is not even perfect. For example what to do with Exceptions or with things that should happen in the moment when you leave the stack frame (e.g. destructors)?

dpsutton 2026-03-26T15:50:48.752289Z

> Tail Calls and Recursion: > I very much would have liked to copy > Scheme’s ‘proper tail recur- sion’ [Steele Jr and Sussman 1978] and > the elegant programming style it supports, but the JVM does not allow > for that stack management approach in any practical way. On the other > hand, I consider partial tail call optimization, e.g., of self-calls > but not mutually recursive calls, to be a semantic non-feature. In > addition, even with proper tail recursion, I thought it may be a point > of confusion among users when exactly a call is in tail position and > subject to the optimization.

dpsutton 2026-03-26T15:51:38.944709Z

section 3.2.3 from https://clojure.org/about/history pdf has great design decisions like this.

dpsutton 2026-03-26T15:54:56.379369Z

this thinking might be a hint at the first as well

Jan Šuráň 2026-03-26T16:14:21.703529Z

I stil don't quite understand, why optimizing tail recursive calls should be a non-feature if it's just an optimization. In the end I care about whether my code doesn't fail and whether it's efficient, it does not break the code in any way. It should be very easy to detect, but sure, it's not as straightforward as the original issue. Definining general recursive functions using defn (where the recursive call doesn't have to be in the tail position), is more common. There I really don't understand, why the inner function isn't named. The change is literally just replacing

(cons `fn fdecl)
with
(cons `fn (cons name fdecl))
(or possibly some meta attached to the name).

dpsutton 2026-03-26T16:16:02.554879Z

unpredictable optimization is the issue i believe. If you think something should be a loop but it turns out at runtime to be a recursive call that can break unpredictably. Using recur ensures that you know precisely when you are consuming stack or not

Jan Šuráň 2026-03-26T16:20:39.281679Z

How could this possibly break something? Using loops to eliminate tail recursion is just an optimization and it does not change what the program does. Instead of creating a new stack frame, it just replaces locals inm the current frame, but that does not break anything.

dpsutton 2026-03-26T16:21:42.893029Z

it does if you process 20,000 items. optimized it works, deoptimized it fails

Jan Šuráň 2026-03-26T16:23:03.165029Z

So your worry is about the "it works on my machine" thing if the compiler decides to optimize it during development, but for some reason doesn't optimize the same code in production?

dpsutton 2026-03-26T16:30:06.188699Z

I’ll just point back to the authoritative source: the jvm doesn’t particularly love proper tail recursion, and the partial solutions didn’t appeal to the author. He made it explicit so it is never a surprise and gives this: > An advantage of recur is that the compiler reports an error if it > occurs in other than a tail position. This has been helpful in > practice—imperative programmers struggling to find how to do iteration > search for ‘loop’ and find it, then get a gentle introduction to its > recursive/ functional nature and help getting it right (the tail > check).

Jan Šuráň 2026-03-26T16:38:44.622469Z

Well, I'm still confused, why having the compiler optimize code for you when it doesn't break anything is a "bad surprise"

Jan Šuráň 2026-03-26T16:39:13.088929Z

But ok, if you think otherwise, there's still the main question that I've asked - Why doesn't defn implicitly name the inner function?

dpsutton 2026-03-26T16:39:25.474879Z

there’s something like that that comes up as well. last is always O(n), even on vectors where it could be O(1)

dpsutton 2026-03-26T16:39:45.617659Z

clojure.core/last
([coll])
  Return the last item in coll, in linear time

dpsutton 2026-03-26T16:40:17.243139Z

the idea there is that if you are using a vector, it’s easy to swap to something that isn’t indexed and then performance can be catastrophic. Predictability is desirable, even if it could be faster.

Jan Šuráň 2026-03-26T16:43:49.481929Z

Yeah, this is not ideal, but I think this is more similar to questions like "why doesn't clojure.set/union check for its argument types?" - checking the type at runtime would introduce some overhead that mostly is not necessary (because the arguments are expected to be sets). However, when optimizing tail recursive calls, there isn't really any overhead.

dpsutton 2026-03-26T16:44:17.128149Z

nor for last being o(1) on vectors

dpsutton 2026-03-26T16:44:38.443229Z

that’s not garbage input, it is forgoing an optimization to give you the last element in o(1) time and traverses the structure

Jan Šuráň 2026-03-26T16:44:46.281469Z

Well, the answer to this is the peek function

Jan Šuráň 2026-03-26T16:45:14.742219Z

And mostly you know you're working with vectors so you can use the peek function directly.

dpsutton 2026-03-26T16:45:31.008349Z

of course

Jan Šuráň 2026-03-26T16:54:13.654049Z

How about the original question then? Why doesn't defn implicitly name the inner function?

dpsutton 2026-03-26T16:55:06.132859Z

i believe the decision was made to make it a recur target so the non-stack consuming loop was obvious rather than implicit. makes it more uniform. unless you are just talking about skipping the var indirection?

Jan Šuráň 2026-03-26T16:55:31.477599Z

I'm talking about skipping the var indirection

dpsutton 2026-03-26T16:56:02.178749Z

this is valid and would break i guess?

(defn f
  [x]
  (if (zero? x)
    :done
    (with-redefs [f (fn [y] :exit-early)]
      (f x))))

dpsutton 2026-03-26T16:56:22.485519Z

(f 1) for me terminate, for you would be an infinite loop

Jan Šuráň 2026-03-26T16:56:34.047039Z

I've also crafted a code that would break

dpsutton 2026-03-26T16:57:05.480419Z

that would break under your proposed change?

Jan Šuráň 2026-03-26T16:57:19.408609Z

But, I mean, who would ever even think about writing code like this without having the only intention to break it?

dpsutton 2026-03-26T16:58:02.301509Z

dunno. but an optimization to shave negligible time causes semantic issues isn’t a great starting point i think

Jan Šuráň 2026-03-26T16:58:51.332649Z

Like sure, we just showcased a quick example that would break it, but is there really a single line of Clojure code that relies on this?

Jan Šuráň 2026-03-26T16:59:27.339279Z

With a different intention other than breaking the code

dpsutton 2026-03-26T17:00:28.652999Z

one thing to check? if you compile with direct linking do you get the performance gain you are advocating for? Perhaps this is even already a knob you control

Jan Šuráň 2026-03-26T17:00:57.632699Z

Like, why would anyone ever try redefining a code that is currently being executed? Who would ever write a recursive function that does not rely on itself, but would rather call a completely difrferent implementation of some bad guy decides to change it?

dpsutton 2026-03-26T17:02:36.118529Z

(defn foo
  [x]
  (letfn [(foo [y] (println "shadowd by letfn") :letfn)]
    (let [foo (fn [x] (println "shadowed by let") :let)]
      (foo x))))
what does this return under your optimization?

Jan Šuráň 2026-03-26T17:04:36.884119Z

(defmacro defn-2 [name & fdecl]
  `(def ~name (fn ~name ~@fdecl)))

(defn foo
  [x]
  (letfn [(foo [y] (println "shadowd by letfn") :letfn)]
    (let [foo (fn [x] (println "shadowed by let") :let)]
      (foo x))))
=> #'mila-lang.core/foo
(defn-2 foo-2
  [x]
  (letfn [(foo [y] (println "shadowd by letfn") :letfn)]
    (let [foo (fn [x] (println "shadowed by let") :let)]
      (foo x))))
=> #'mila-lang.core/foo-2
(foo 12)
shadowed by let
=> :let
(foo-2 12)
shadowed by let
=> :let

Jan Šuráň 2026-03-26T17:05:32.914089Z

What do you mean by direct linking?

dpsutton 2026-03-26T17:05:46.500289Z

direct linking i think can remove the var indirection

Jan Šuráň 2026-03-26T17:10:54.012199Z

Oh, ok, this is very interesting...

Jan Šuráň 2026-03-26T17:22:00.007419Z

Thanks for the reference! This indeed solves this issue!

dpsutton 2026-03-26T17:30:56.720859Z

excellent!

exitsandman 2026-03-26T19:57:31.183159Z

btw, a fun fact:

(fn fib [n]
  (case n
    0 0
    1 1
    (+ (fib (dec n))
       (fib (- n 2)))))
does in fact call itself directly, because fib in this case is the fn instance (verified by decompiling)., and this should even work across arities. The root cause of this not happening on a defn is that, ignoring docs and meta, defn basically destructures its contents as [name & fn-body] and expands into (def name (fn @fn-body)` with the fn object not knowing about itself. TCO was deliberately left out to avoid random stack overflows on would-be tail calls that in fact aren't. I feel that recur is a better idea either way, it's more recognizable, and makes you think twice before calling self. Not that raw recursion is particularly common in your usual Clojure code, at any rate.

lukasz 2026-03-26T19:05:41.380509Z

This was a surprising finding - symbols implement clojure.lang.IFn - is this by design? Is it because symbols can be map keys and have to be support this syntax

(def a-map {'foobar :test})
('foobar a-map) ;=> :test
It tripped me up because I need to accept either a qualified symbol (to pass to requiring-resolve) or a function as an argument - so I can't use ifn?` on it's own to check if the inputs are valid.

grzm 2026-03-26T19:19:12.018409Z

Would using fn? instead of ifn? work for you in this case? (FWIW, I would have reached for fn? by default)

p-himik 2026-03-26T19:19:38.803729Z

It's by design, yes - symbols, just like keywords, look themselves up.

👀 1
lukasz 2026-03-26T19:20:47.945639Z

@grzm it would, but as it happens sometimes the code gets #' passed and that doesn't work with fn?

p-himik 2026-03-26T19:22:06.432559Z

You could go the other way around - symbol? (maybe even qualified-symbol?) with an (assert (ifn? ...)) in the "else" branch.

➕ 1
lukasz 2026-03-26T19:32:19.754519Z

Yes, that was the solution in the end - but it took a few minutes to figure out. Thank you all

Ed 2026-03-27T11:03:16.127089Z

#' is a var, not a symbol. Maybe it's worth checking to see if it's derefable and see if the thing inside the container is a fn??

shaunlebron 2026-03-26T20:44:28.471919Z

are there any open issues for clojure to add multiple bindings to if-let when-let if-some when-some? EDIT: vote here https://ask.clojure.org/index.php/3786/allow-multiple-bindings-for-if-let-when-let-some-and-when-some

dharrigan 2026-03-26T20:52:47.739819Z

https://clojure.atlassian.net/browse/CLJ-2213

🙏 2
dharrigan 2026-03-26T20:53:51.095219Z

older one: https://clojure.atlassian.net/browse/CLJ-2007

seancorfield 2026-03-26T21:09:19.122129Z

You can vote for the Ask: https://ask.clojure.org/index.php/3786/allow-multiple-bindings-for-if-let-when-let-some-and-when-some

🙏 1
shaunlebron 2026-03-26T21:53:55.057449Z

now we have 2 votes 🙂

shaunlebron 2026-03-26T21:55:09.744859Z

better-cond uses multiple bindings for when-let and others, and I’ve seen people here mention having a custom when-lets, and we have one in our codebase too.

dharrigan 2026-03-27T07:34:00.813289Z

I've added my vote too 🙂 Oh, and I have a similar when-let macro in my codebase(s) too 🙂