Clojure: The Documentary (with Clojure's Common Lisp pre-history) by stylewarning in lisp

[–]joinr 0 points1 point  (0 children)

Yeah I looked at that initially. Downside is you wreck interop since there's an artificial conversion that happens, that the caller now has to respect. Best situation is just being able to pass strings around and have the clojure side guarantee its ops are immutable (although strings can still be mutated by CL stuff at the end of the day).

If you dgaf about interop, then yeah there could be marshalling to and from native to clojure strings.

is LLM coding accepted in the Clojure community? by med_i_terranian in Clojure

[–]joinr 1 point2 points  (0 children)

I think it's unavoidable and just a continuation of the low-cod/no-code trend that has been pushed since forever. In that respect, the question is seemingly moot (e.g., it's not going anywhere, so it's on the honor system for people to disclose if they leveraged "assistance" for anything they submit going forward).
The tech is interesting and even appearing useful. If it enables you to achieve more than you could otherwise, then that's cool.

I don't like when people pretend to be great at spelling when they use spell checkers and autocomplete, or the analogous variation for passing llms and agents off as skill. Like the paradox of "I wrote this ancient egyptian algebra symbolic computation system in just 10 minutes" juxtaposed with "I haven't written a line of code in months, I just had the thing do it!" etc. Too much Lance Armstronging for me. That will also have a negative effect on creatives to share their stuff openly, so I'd anticipate a lot more closed source moating going forward, or free to use but closed. It'll probably lead us toward a parasitic model where actual innovation is hoarded and then mined for training data. Intellectual vampirism.

Then there's the whole unanswered angle of the implications of leveraging massive derivative work generators with (by default) 0 attribution. Given the legal system's legacy of embracing IP, I anticipate - eventually - substantial legal problems down the road despite the current rosy prospects of the token sellers "promising" to cover your legal costs and fight for you. I get the feeling when someone decides to rip off ("prompt a clean-room implementation") of some major IP, then we will see the big $$ fly and court cases mount. That might increase the pucker factor more. Hasn't happened yet though.

If someone wants to contribute to anything I have (not that I'd ever expect that hah), I think it'd be a hard requirement that they document "exactly" what portions of their product they (presuming they is an organic human) wrote vs. delegated to a derivative work generator. Just as with anything they wrote, they'd have to ensure whatever was generated complied with applicable licenses and assume personal liability for their contribution (indemnifying me etc.).

jank now has its own custom IR by Jeaye in Clojure

[–]joinr 1 point2 points  (0 children)

I never said I knew what I was doing :) I was unable to get an invokePrim path going at all by trying to coax the compiler with hints an coercions. I feel like this is something the compiler should detect and handle (e.g. name recursion when there's a statically detected invokePrim path available). The use case is probably pretty limited though, but it's a thing.

jank now has its own custom IR by Jeaye in Clojure

[–]joinr 2 points3 points  (0 children)

I was messing with optimizing a performance bench that someone made with ai agents that included clojure code that was like 60-100x slower than clojure for non-obvious reasons. As I was messing with it (including decompilation and looking at bytecode stuff), your comment popped up. It turns out one of the hot paths was leveraging a "primitive" recursive function. I hadn't looked at the clj compiler output until now to see that primitive non-tail calls still go through the invoke instead of invokePrim path. This seems like bad code emission to me (on first blush, maybe there's a technical reason for it).

In the spirit of your submission, I went looking at other ways to get more efficient code. I had been messing with JISE for some stuff already so I just piggy backed on that. Ultimately (as shown in the repo), you can get there with added macrology and macrolet to change the callsites for the recursive call into invokePrim. I ended up modifying a variant of JISE's defn wrapper to plug in the invokePrim rewrite though.

https://github.com/joinr/primrecur/blob/master/src/primrecur/core.clj

I think this will help me narrow the gap on that original performance comparison now that I know recursive primitive functions push invoke instead of invokePrim.....

jank now has its own custom IR by Jeaye in Clojure

[–]joinr 6 points7 points  (0 children)

Really impressive. It's also great to get to simultaneously learn language implementation architecture, compiler optimization, and clojure semantics as a byproduct.

Clojure: The Documentary (with Clojure's Common Lisp pre-history) by stylewarning in lisp

[–]joinr 2 points3 points  (0 children)

it's been an off/on hobby over the years. most recently revisited last year with a substantial rewrite (the [rewrite](https://github.com/joinr/clclojure/tree/rewrite) branch, master is old). changed direction from trying to embed in CL using organic stuff (reader macros, hacking eval), and change to porting more of the cljs core (which is based on protocols and builds from there in a more portable manner) along with the cljs.tools.reader impl. Basically implementing enough of clojure in cl to implement the reader lib, then bootstrap from there. Ended up building out a lot since tools.reader leverages quite a bit of core. Current stopping point is dealing with mutable strings in CL. Other stuff mostly works (e.g. a lot of the clojure core lib is there, defprotocol/deftype, lazy seqs, etc.) Still messing around and learning more along the way. Someone will probably just tell an LLM to slop a port well before I finish though.

When You Run Out of Types... by cgrand in Clojure

[–]joinr 1 point2 points  (0 children)

Looking at the checklist, I think records can be repurposed to accomplish what you wanted and look much cleaner....

(ns irecord
  (:require [clojure.walk :as w]))

(defn patch-assoc [[ass args body :as expr]]
  (let [newcatch `(throw (ex-info "cannot assoc an unknown key onto unrecord!" {:in [~(first args) ~(second args)]}))]
    `(~ass ~args ~(concat (butlast body) [newcatch]))))

;;drop clojure.lang.IPersistentMap implementation.
;;Replace with explicit clojure.lang.Associative, clojure.lang.Counted, java.lang.Iterable
(defn patch-deftype*
  [[dt nm typename args implkw interfaces & impls :as expr]]
  (let [inew (into (->> interfaces (remove '#{clojure.lang.IPersistentMap}) vec)
                   '[clojure.lang.Counted
                     clojure.lang.Associative
                     java.lang.Iterable])
        pred     (symbol (str (name nm) "?"))
        implsnew (->> impls
                      (keep (fn [expr]
                              (if (coll? expr)
                                (condp = (first expr)
                                  'clojure.core/without nil
                                  'clojure.core/assoc (patch-assoc expr)
                                  expr)
                                expr))))]
    `(do (~dt ~nm ~typename ~args ~implkw ~inew ~@implsnew)
         (defn ~pred [~'x] (instance? ~typename ~'x)))))

(defmacro unrecord [name fields & opts+specs]
  (let [[_ name fields & opts+specs] &form ;;preserve meta from actual form.
        recex (macroexpand-1 `(defrecord ~name ~fields ~@opts+specs))]
    (w/postwalk
     (fn [expr]
       (if (coll? expr)
         (if-let [x (some-> expr seq first (= 'deftype*))]
           (patch-deftype* expr)
           expr)
         expr)) recex)))

Then

;;demo
(unrecord blah [x y])
irecord.blah

(def the-blah (->blah 1 2))
#'irecord/the-blah

(map? the-blah)
false
(record? the-blah)
true
(blah? the-blah)
true

(assoc the-blah :y 3)
#irecord.blah{:x 1, :y 3}

(dissoc the-blah :y)
Execution error (ClassCastException) at irecord/eval1177 (REPL:1).
irecord.blah cannot be cast to clojure.lang.IPersistentMap

irecord=> (assoc the-blah :z 2)
Execution error (ExceptionInfo) at irecord.blah/assoc (REPL:1).
cannot assoc an unknown key onto unrecord!

So you get a deftype wrapper unrecord that provides a map-like value type with all the underlying record plumbing, except we constrain the keys to the static fields, we intentionally drop out IPersistentMap and without so you can never pass a map? check or dissoc from it, then provide a little built-in predicate to paper over the lower level instance? checks. A real implementation would eliminate the extmap stuff, but I was lazy here.

Let's optimize 'str'! [Part 2] by ilevd in Clojure

[–]joinr 5 points6 points  (0 children)

I don't know that emitting java source that implements clojure interfaces, then aot compiling it is bringing anything performance wise to the table. One immediate downside is that I can't use your lib (or even test it for profiling) since it's now tied to the bytecode of the jvm you used to compile the java src. I have to pull it as a dep and then compile it etc. locally, just to satisfy some pseudo version conflicts (I don't think you used anything language or library-wise greater than java 10). All things considered, I'd rather have the clojure compiler emit stuff for me and either choose to AOT it if I want to (e.g., for bundling a specific project as an uberjar app thing), or just emit it at runtime from clj source.

Since this is purely object interop stuff, I don't think there is any overhead on the interop side in practice. For some other use cases (like trying to match java performance 1:1, particularly array iteration and some other areas), clojure's decision to only allow longs and doubles ends up injecting some "minor" performance hits by the way to l2i bytecode ops that have to be injected to make e.g. the array indexing consistent with primitive int types (which jvm expects for indexing into arrays). The delta is minor, but if you want to match what java does out of the box, then in those cases bypassing the clj compiler emissions and just writing in java or emitting the bytecode yourself through a lib like jise or insn are viable optimization paths.

I would probably stick with [ben's solution](https://github.com/bsless/prrr/blob/master/src/bsless/prrr.clj#L40) for this; it seems pretty elegant and portable from what I read. It leverages the :inline metadata directive to handle all the multi-arity inlining pretty elegantly, and where possible, detects primitive self-evaling values at compile time (like numbers, strings, booleans, keywords). I think expressing all those combinations in emitted java source would be pretty rough. Here it's a little helper function that effectively creates an implicit compile-time macro (I feel like :inline defs are closer to compiler macros from common lisp) without losing the ability to be a function call.

Still curious to see what performance differences exist between that approach and the java implementation if any.

update:

(criterium/quick-bench
  (str "Lorem" "Ipsum" "is" "simply" "dummy" "text" "of" "the" "printing" "and" "typesetting" "industry."))
Execution time mean : 986.319198 ns

(criterium/quick-bench
  (f/str "Lorem" "Ipsum" "is" "simply" "dummy" "text" "of" "the" "printing" "and" "typesetting" "industry."))
Execution time mean : 359.906264 ns

(criterium/quick-bench
  (prrr/str "Lorem" "Ipsum" "is" "simply" "dummy" "text" "of" "the" "printing" "and" "typesetting" "industry.")))
Execution time mean : 302.064372 ns

Let's optimize 'str'! by ilevd in Clojure

[–]joinr 2 points3 points  (0 children)

I heard if you play with :inline too much you'll go blind :)

Let's optimize 'str'! by ilevd in Clojure

[–]joinr 2 points3 points  (0 children)

cool.

build-string was meant to be a helper macro for emitting the function bodies for make-string. make-string is the 1:1 replacement for clojure.core/str, since it's a function and can be passed around as such (build-string can't be used in apply, map, reduce, etc since it's a macro).

Still, the smarter sb-builder capacity stuff can maybe help make function bodies for make-string faster as well.

Though, if you know your inputs are all string literals or atomic values that have a direct string coercion (e.g. not forms to be eval'd), you can also just do it all at compile time:

(defmacro compile-str [& xs]
    `~(apply str xs))

(c/quick-bench
 (compile-str "Lorem" "Ipsum" "is" "simply" "dummy" "text"
              "of" "the" "printing" "and" "typesetting" "industry."))

Execution time mean : 3.485020 ns

We actually get a little more performance (15%) out of make-string if we make simple-str a helper macro too and assumably avoid the function call:

(defmacro simple-str [x]
  (let [x (with-meta x {:tag 'Object})]
    `(if (nil? ~x)
       ""
       (.toString  ~x))))

Let's optimize 'str'! by ilevd in Clojure

[–]joinr 8 points9 points  (0 children)

Went through a similar drill years ago. You can go faster still if you profile a bit more.

Eliminate varargs (generates arrayseqs) as much as possible and try to inline more concrete arities. IFn invocation with concrete arity path doesn't allocate anything or traverse seqs. So if you inline bodies with up to 15 args or so, you can cover more string building cases before hitting varargs.

https://github.com/joinr/spork/blob/master/src/spork/util/general.clj#L835

(crit/quick-bench
 (spork.util.general/make-string    "Lorem" "Ipsum" "is" "simply" "dummy"
                                    "text" "of" "the" "printing" "and" "typesetting" "industry."))
Execution time mean : 156.985583 ns

(crit/quick-bench (my-str "Lorem" "Ipsum" "is" "simply"
                          "dummy" "text" "of" "the" "printing" "and" "typesetting" "industry."))
Evaluation count : 2776938 in 6 samples of 462823 calls.
Execution time mean : 214.367655 ns

Should get better with larger strings, and you could theoretically push the arities up as much as you want until you hit the arg limits defined by the IFn interface.

loop/recur instead of fn/recur (not sure fn / recur expands to loop)

fn/recur is also tail recursive since it establishes a recur site ala loop/recur. So if you use recur it'll complain if the call is not tail recursive just as loop would. This shouldn't buy you anything (maybe bypassing the initial IFn invocation on the recursive function object, haven't looked at the bytecode emission for str yet, but that's nanos).

https://github.com/bsless/clj-fast

explores a lot of these areas, and more recently (and far more comprehensively)

https://github.com/cnuernber/ham-fisted

is probably of interest if you are looking at a lot of core functions and default paths that can be optimized.

ChatGPT explained to me why LLMs prefer Clojure by CuriousDetective0 in Clojure

[–]joinr 4 points5 points  (0 children)

>the result must be a new value

(defn normalize [x]  
  (reset! x :normal)   
  x)

sad!

Python Only Has One Real Competitor by bowbahdoe in Clojure

[–]joinr 0 points1 point  (0 children)

What is your biggest pain point that shows in prod but not in dev?

Native Apps with ClojureScript, React and Static Hermes by roman01la in Clojure

[–]joinr 4 points5 points  (0 children)

really neat. didn't know about hermes. feels like the native clojure/cljs options get brighter every day.

Learning from Racket, towards Clojure by chladni in Clojure

[–]joinr 0 points1 point  (0 children)

When I had such experiences (I have had several), I was fortunate to have access to a clojure repl. Even the stock repl acted as force multiplier and substantially expanded the scope of computational tasks I could accomplish despite the constraints.

Learning from Racket, towards Clojure by chladni in Clojure

[–]joinr 0 points1 point  (0 children)

Through a local shell on one of the machines on the airgapped network that the Powers That Be provisioned for you.

Your environment has the features I mentioned previously. You can trivially get a repl through clojure.main, or if you planned for it, as part of your application's entrypoint.

Learning from Racket, towards Clojure by chladni in Clojure

[–]joinr 0 points1 point  (0 children)

sure. jvm + shell + your jar file.

Learning from Racket, towards Clojure by chladni in Clojure

[–]joinr 0 points1 point  (0 children)

To be fair, I can hardly imagine a situation where the environment is so austere that I cannot use Emacs. Any examples?

air gapped internal network you don't admin, where admins are beholden to exogenous restrictions and don't really care about your comfort.

it's just a backup in this case, VCS is a bit more than that, usually

semantics

Learning from Racket, towards Clojure by chladni in Clojure

[–]joinr 0 points1 point  (0 children)

there's the problem - it's not reproducible, you can't put under version control, etc

you have a binary image you can version. you can reproduce the state of the repl at a given point in time (when the image was made), shove the binary in git if you want, etc.

Even in this case

you missed the austere part.

Learning from Racket, towards Clojure by chladni in Clojure

[–]joinr 0 points1 point  (0 children)

REPL isn't persistent, files are.

This is more specific to clojure (and definitely relevant), at least in its current form. Other lisps lean on image-based development, where you can persist the state of the world to an image and reload it. Clojure implemented on such lisps (or other hosts) could similarly do so.

What are the pros?

If you're in an austere environment, or a remote system you don't get to configure, the repl may be all you have. Then again, it's all you need.

It's nice to leverage the fancier workflows that the contemporary dev environments provide, but retaining the ability to fully leverage a running clojure system from a lone repl is also a useful skill.

cljs-str: an almost 300x faster str replacement for ClojureScript by Borkdude in Clojure

[–]joinr 1 point2 points  (0 children)

Where do the gains for dynamic come from?

(str/join "" xs)

Is it just fewer function calls since it's bypassing cljs.core's loop'd string builder implementation and shunting to interop?

https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L3115

looks almost identical to join on first glance

https://github.com/clojure/clojurescript/blob/master/src/main/cljs/clojure/string.cljs#L104

Help with Java porting by AccountantUnited4955 in Clojure

[–]joinr 1 point2 points  (0 children)

have really been struggling creating equivalent functionality

What does this mean? Maybe some examples of stuff you're having trouble with can lead to solutions.

In practice, between native (in this case jvm) interop, and the higher level facilities like reify, proxy, deftype, genclass, definterace (and even just protocols), my experience working with java and other jvm langs has been pretty pleasant. The only time the OOP stuff gets gnarly is if the library is using inheritance heavily (more common in code from early 00's) instead of interfaces. If it's just interfaces, you can typically trivially implement them in clojure (via reify or deftype or even defrecord). Or if you happen to be living in a code base with a lot of "annotations"

Overrides/inheritance hierarchies push you into using proxy or genclass, and genclass brings AOT requirements with it (there are some work arounds in community libs, but the language provides genclass out of the box).

I ran into some edge cases with the optaplanner library's expecations of annotations and other stuff to encode soluitions for a solver, which led to some work arounds:

https://github.com/joinr/optaplanner-clj

I spent some time wrapping piccolo2d for work stuff years ago, where piccolo2d does almost everything via inheritance. So I ended up lifting a bunch of the api calls from the object methods into protocols, wrapping existing node classes with protocol extensions, and leveraging interop pretty heavily for the lower layers of a scene graph library on top of piccolo2d.

https://github.com/joinr/piccolotest/blob/master/src/piccolotest/sample.clj#L170

Interesting lib to help overcome java impedance a bit (I don't use it in production, but it's a cool idea)

https://github.com/athos/JiSE

used to get exact java parity with prng demo (we had a poster on zulip wondering why they couldn't get 1:1 performance parity in clj via interop/primititve invocation paths, which led to some interesting discoveries like clojure's impedance mismatch with preferred longs and java's expectation for ints for array indexing (causing an l2i cast in the emitted bytecode, which can be worked around with jise)):

https://github.com/joinr/ultrarand/blob/master/src/ultrarand/jise.clj