Fork me on GitHub
#beginners
<
2023-09-21
>
Eli Pinkerton02:09:17

Hey folks! Clojure beginner here. I'm writing some data parsing logic - I have data in format "foo" and want to translate it to format "bar". Both of these data structures are maps that I manually relate keys between. The code looks something like this:

(defn parse-user [user-json]
  {:user/id (-> user-json :id str)
   :user/avatar-url (:avatar_url user-json)
   :user/url (:url user-json)
   })
Is there a more idiomatic way of doing this? My problem is that "user-json" gets passed to all of the mapping code, can this be done in a cleaner fashion?

Bob B02:09:28

clojure.set/rename-keys can do the direct renames, or if it's just qualifying, it could probably be done with update-keys

Bob B02:09:34

simplified examples:

(let [m {:id 5
         :name "Tim"}]
  [(set/rename-keys m {:id :new-id :name :new-name})
   (update-keys m #(keyword "user" (name %)))])
=> [{:new-id 5, :new-name "Tim"} #:user{:id 5, :name "Tim"}]

Eli Pinkerton02:09:00

Nice, those are pretty close to what I want. For some keys I want to transform the data. So it sounds like I'd do a combination of: • rename all the keys to be what I expect • update-keys to do data transforms

Bob B02:09:44

and with the stringifying, you could thread that:

(let [m {:id 5
         :avatar-url "ava.tar"
         :url "u.rl"}]
  (-> m 
      (update :id str)
      (update-keys #(keyword "user" (name %)))))
=> #:user{:id "5", :avatar-url "ava.tar", :url "u.rl"}
if you have a bunch, you could maybe reduce with update over a list of keys (if you're doing the same transform) or a list of keys and transforms if you need to stringify some keys and do other things with other keys

Bob B02:09:36

you'd want update-vals to update the values in the map, but update-vals will do every val in the map, so if you want to transform values selectively, it'll need to be more specific. Unless you happen to just be calling str on things, because str on a string just returns the string, so it'd be possible to update-vals with str without changing string values

tschady15:09:24

consider #CFFTD7R6Z , especially if this gets more complex

Eli Pinkerton20:09:24

Awesome, really appreciate the tips!

anovick04:09:37

Is there some way to keep track of the Clojure functions I send to the REPL? I have an idea where the REPL evaluates the expressions I send to it and every time a function/macro is sent, it keeps track of that usage in a database (adds an entry for the function's fully-quallified name, and initializes a counter let's say) This way I can derive meaning of my code usage for my own learning journey and improvement. Examples for use-cases: • usage as a persistent statistics/history tool: query my top 20 used functions in my last 1000 REPL entries (let's say I forget stuff I used), or the bottom 20 used functions • helps me curate a white-list or a black-list of functions • make an integration with a code editor to auto-complete only functions in my white-list or auto-complete everything except for black-list ones • suggest to me popular or (somehow) otherwise useful functions that I'm not using Is there something like it already?

anovick06:09:52

@U8LB00QMD That looks like a cool project. Could you perhaps point to how this could tie in to what I was describing?

Bobbi Towers06:09:05

It's what I most often hear folks using to augment the REPL experience

anovick06:09:08

Yea I can tell from a glance at the project page and introductory video. However it seems mainly about exploring data within REPL sessions. On the other hand, I'm describing some persistence of metadata about the sort of functions (language feature) that I'm using (at the REPL) across sessions

anovick06:09:27

Quite different beasts

anovick07:09:54

Guess something like this doesn't exist yet, and I could perhaps take a stab at implementing some proof of concept of it sometime.

Bobbi Towers07:09:26

Yeah you described a rather specific set of features that's probably not been implemented yet. I just mentioned Portal because it seems like the closest thing, and might serve as inspiration

🙌 1
flowthing08:09:38

I don’t think anything like this exists, but it’s a very cool idea. If your editor supports plain clojure.main REPLs, you could build something like this by customizing the R step of the REPL. With nREPL, since you can’t swap in your own R, you’d have to write your own version of the interruptible-eval middleware and use that, but editor support for that sort of thing varies, I think.

flowthing08:09:00

I’ve thought about making something along these lines (maintaining a queryable REPL history). I think implementing something like that on top of a Datalog database would be cool, but I’m not sure whether any of them supports storing S-expressions as is.

flowthing08:09:57

There’s a little bit of prior art here, I think: https://github.com/dpsutton/grepl

seancorfield15:09:24

clj and lein repl both track REPL input in a history file so you could probably write something that could query over that (by reading it as EDN or Clojure forms).

anovick16:09:55

@U4ZDX466T Thanks for your comments! Re: "tooling integration" Unfortunately I'm not very knowledgeable on the technical side of the tooling technologies (editors, REPLs). I mainly use Cursive when programming Clojure, but I believe that's closed source? perhaps it's difficult to make an integration with it w.r.t the features I listed (auto-completing customization via white-list / black-list, have it sort things via frequency of usage, have it send notifications about suggested functions to use, etc.) I have used VSCode's (with Calva) a long time ago (3 years maybe?) and at that time it lagged behind in static analysis capabilities which was the reason I ditched it. However, it is open-source and therefore can be worked with to develop stuff like these, so I'm considering going that route. I may be required to alter something in the REPL technology too, but I don't have a clue about that yet. Also, I like the project your linked, it is very relevant and I'll consider learning from it :)

seancorfield16:09:56

Calva uses clojure-lsp and clj-kondo these days for static analysis so it's pretty much best-in-class now.

anovick16:09:48

@U04V70XH6 That's interesting to know! However I may wish to have more extensive data stored about the code sent than just the history of it being sent (Having a counter, a date, belonging to a white-list / black-list etc.)

anovick16:09:53

@U04V70XH6 Re: "Calva" I believe clj-kondo existed even back when I tried it 3+ years ago, I also remember trying to use it with a VSCode extension (maybe it was Calva) but at the time that interaction didn't work even a single time. It did work through the CLI I believe but that wasn't useful enough on its own. Did these tools (clj-kondo and Calva) really improve that drastically in recent years?

seancorfield16:09:34

Yes, night and day.

🙏 1
anovick16:09:44

Interesting! I'll give it a try again :)

Jim Newton06:09:54

I just realized something really cool about the Clojure language. Take a look at this function:

(defn power [b p]
  (cond (= p 0)
        1

        (= p 1)
        b

        (even? p)
        (recur (* b b) (/ p 2))

        :else
        (* b (power b (dec p)))))
What is interesting is that I (the programmer) can decide which call is tail-recursive and which is not. Does this really compile like I think it does? The call to recur is compiled as a jump, but the call to power is compiled as a function call?

jpmonettas11:09:15

Yes, here you have the java decompilation :

public final class dev$power extends AFunction
{
    public static final Object const__2;
    public static final Var const__3;
    public static final Keyword const__7;
    public static final Var const__8;
    
    public static Object invokeStatic(Object b, Object p) {
        while (!Util.equiv(p, 0L)) {
            Object o;
            if (Util.equiv(p, 1L)) {
                o = b;
            }
            else {
                final Object invoke = ((IFn)dev$power.const__3.getRawRoot()).invoke(p);
                if (invoke != null) {
                    if (invoke != Boolean.FALSE) {
                        final Object multiply = Numbers.multiply(b, b);
                        p = Numbers.divide(p, 2L);
                        b = multiply;
                        continue;
                    }
                }
                final Keyword const__7 = dev$power.const__7;
                if (const__7 != null) {
                    if (const__7 != Boolean.FALSE) {
                        o = Numbers.multiply(b, ((IFn)dev$power.const__8.getRawRoot()).invoke(b, Numbers.dec(p)));
                        return o;
                    }
                }
                o = null;
            }
            return o;
        }
        return dev$power.const__2;
    }
    
    @Override
    public Object invoke(final Object b, final Object p2) {
        return invokeStatic(b, p2);
    }
    
    static {
        const__2 = 1L;
        const__3 = RT.var("clojure.core", "even?");
        const__7 = RT.keyword(null, "else");
        const__8 = RT.var("dev", "power");
    }
}
and the bytecode one :
public static java.lang.Object invokeStatic(java.lang.Object b, java.lang.Object p);
        Flags: PUBLIC, STATIC
        Code:
                  linenumber      2
               0: aload_1         /* p */
               1: lconst_0       
                  linenumber      2
               2: invokestatic    clojure/lang/Util.equiv:(Ljava/lang/Object;J)Z
               5: ifeq            15
               8: getstatic       dev$power.const__2:Ljava/lang/Object;
              11: goto            115
              14: athrow         
                  linenumber      2
              15: aload_1         /* p */
              16: lconst_1       
                  linenumber      5
              17: invokestatic    clojure/lang/Util.equiv:(Ljava/lang/Object;J)Z
              20: ifeq            28
              23: aload_0         /* b */
              24: goto            115
              27: athrow         
                  linenumber      2
              28: getstatic       dev$power.const__3:Lclojure/lang/Var;
              31: invokevirtual   clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
                  linenumber      8
              34: checkcast       Lclojure/lang/IFn;
              37: aload_1         /* p */
                  linenumber      8
              38: invokeinterface clojure/lang/IFn.invoke:(Ljava/lang/Object;)Ljava/lang/Object;
              43: dup            
              44: ifnull          73
              47: getstatic       java/lang/Boolean.FALSE:Ljava/lang/Boolean;
              50: if_acmpeq       74
              53: aload_0         /* b */
              54: aload_0         /* b */
                  linenumber      9
              55: invokestatic    clojure/lang/Numbers.multiply:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Number;
              58: aload_1         /* p */
              59: ldc2_w          2
                  linenumber      9
              62: invokestatic    clojure/lang/Numbers.divide:(Ljava/lang/Object;J)Ljava/lang/Number;
              65: astore_1        /* p */
              66: astore_0        /* b */
              67: goto            0
              70: nop            
              71: nop            
              72: athrow         
              73: pop            
                  linenumber      2
              74: getstatic       dev$power.const__7:Lclojure/lang/Keyword;
              77: dup            
              78: ifnull          113
              81: getstatic       java/lang/Boolean.FALSE:Ljava/lang/Boolean;
              84: if_acmpeq       114
              87: aload_0         /* b */
              88: getstatic       dev$power.const__8:Lclojure/lang/Var;
              91: invokevirtual   clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
                  linenumber      12
              94: checkcast       Lclojure/lang/IFn;
              97: aload_0         /* b */
              98: aload_1         /* p */
                  linenumber      12
              99: invokestatic    clojure/lang/Numbers.dec:(Ljava/lang/Object;)Ljava/lang/Number;
                  linenumber      12
             102: invokeinterface clojure/lang/IFn.invoke:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
                  linenumber      12
             107: invokestatic    clojure/lang/Numbers.multiply:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Number;
             110: goto            115
             113: pop            
             114: aconst_null    
             115: areturn        

Jim Newton06:09:34

Yes, I think line 67 "goto 0" is the (recur ....) call.

Jim Newton06:09:34

To me that is really interesting, because in some other languages that I use regularly, Scala for example, either all the calls are compiled as tail calls, or none are. I might be mistaken, but that is how I understand it. In Common Lisp, of course it depends on the compiler, but SBCL for example will compile tail calls if possible and leave others as simple calls whenever the programmer requests a certain level of optimization.

Jim Newton06:09:29

I'm preparing a beginner course in clojure programming for first year engineering students. This will be an interesting discussion point when contrasting various functional languages.

👍 1
💯 1
Jim Newton06:09:42

preliminary testing seems to indicate it works this way.

huy08:09:12

How to split a sequence whenever an element satisfies a predicate? For instance, split [2 3 4 5 6 7 8 9 10] with separators being divisible by 3, result will be [[2] [3 4 5] [6 7 8] [9 10]]

tomd08:09:25

This was asked quite recently. lmk if this thread helps: https://clojurians.slack.com/archives/C053AK3F9/p1694640884780829

gratitude-thank-you 1
huy09:09:22

@UE1N3HAJH thank you, that is really handy 🙂

👍 1
Nikolas S.10:09:29

I hope I am not asking too many questions in this channel! I have a function with side effects, that fetches data. In the past I did not consider using threads, but now I want to allow the user run this function in parallel in multiple threads with different values. (I.e. fetching the data from different sources) I would like to store and periodically update the current progress for each value/source to allow seeing the progress via an API endpoint. For that I wanted to have a mutable list of maps, i.e. ({:name "A" :progress 5} {:name "B" :progress 15} {:name "C" :progress 60}) Before I was using threads, I was using a simple atom that just stored a number. But now I am not sure I should continue using atoms or opt something else since: The documentation for swap! says:

Usage: (swap! atom f)
       (swap! atom f x)
       (swap! atom f x y)
       (swap! atom f x y & args)

Atomically swaps the value of atom to be:
(apply f current-value-of-atom args). Note that f may be called
multiple times, and thus should be free of side effects.  Returns
the value that was swapped in.
So If I have a list of maps and following call: (swap! myatom conj {:name "Test" :value (function-with-side-effects) }) Is there any danger of the side effect occurring again, meaning that function-with-side-effects is executed again? Or is its return value stored and only the conj function may be called multiple times ? Could I mitigate this issue, by storing the value of function-with-side-effects in a dynamic variable and then calling the swap! operation using this variable? Or should I opt for using agents or refs (althought I have heard they are rarely used)? I hope that makes sense.

delaguardo11:09:50

your function will be executed before swap! it is conj that can be executed multiple times but the arguments to it will be the same.

❤️ 1
💡 1
Nikolas S.12:09:35

Got it, then I have nothing to worry about that's good to know

daveliepmann18:09:07

> I hope I am not asking too many questions in this channel! Nope. Ask away :)

❤️ 1
Sam11:09:50

Is there a way to get :or kvs to work with :as in function arguments?

(defn foo [{:keys [a b]
            :or {b "b"}
            :as m}]
  m)

;; Can I get this to include b?
(foo {:a "a"});; => {:a "a"}

delaguardo11:09:29

not with destructuring but you can always do something like this:

(defn foo [{:keys [a b]
            :or {b "b"}
            :as m}]
  (let [m (assoc m :b b)]
    m))

❤️ 1
Sam11:09:46

Yeah, that works. It would have been cool to be able to do it all at the beginning, but to be honest maybe that would have been to messy anyway. Thank you! 🙂

delaguardo11:09:28

"Clojure way" ™ is to avoid mutations

Sam11:09:15

(defn foo [{:keys [a b]
            :or {b "b"}
            :as m}]
  (let [m' (assoc m :b b)]
    m'))
<<functional programming achieved>>

👍 1
Noah Bogart13:09:52

if i need the :or defaults to exist in the :as map, i don't destructure the arg, i destructure in a let block below:

(defn foo [m]
  (let [m-defaults {:b "b"}
        {:keys [a b] :as m} (conj m-defaults m)]
    ...))

Sam10:09:18

Nice solution, thank you!

Alice Trinta14:09:11

Hey, guys! I have a question about tests, I'm using https://github.com/nubank/state-flow to write my integration tests and does anyone know if this tool supports asserts of function calls? Like, if I wanted to see if some function was called during the execution of the test, could I assert this? I know that in other languages, we have testing tools that provide this, but idk if stateflow does it too.

respatialized15:09:36

I don't know much about stateflow, but I think you may be able to use with-redefs to wrap your function so you can test if it was called in the context of the test. Something like:

(def called-fns (atom {}))
(t/deftest fn-call-test
 (t/testing "context"
  (with-redefs 
   [my-fn (fn my-fn-wrapped [arg]
             (swap! called-fns assoc :my-fn true)
(my-fn arg)
)]
; do integration test stuff ...
(t/is (:my-fn @called-fns) "my-fn should be called")
)))

1
shiyi.gu15:09:57

Hello there, what's the best practice to work with index for matrix in clojure. I am trying to do leetcode like practice with using clojure.

shiyi.gu15:09:33

like maze exploration, it usually needs to get the index of a value in the matrix, or using index to traverse the matrix.

Bob B16:09:21

interop (`.indexOf`) is an option, or map-indexed or keep-indexed to search, and get and get-in are for 'traversing', depending on precisely what's desired

anovick20:09:09

I'm trying to figure out the conceptual model for the REPL and Clojure compilation steps. On the JVM, a Clojure project is first compiled to bytecode so that it can run as a JVM program. However, when the resulting JVM program runs, a Clojure runtime is running in the background. The REPL is a UI for that runtime. Sending code to the REPL makes the runtime evaluate that code without additional compilation steps. Is this accurate? maybe I'm wrong on something, I'm not sure 🙂

respatialized20:09:34

I'm not 100% certain about this, but I don't think it's strictly accurate to speak of a Clojure "runtime." Certainly running Clojure involves importing lots of Java classes, but those are still Java/JVM objects, not another intermediate layer on top.

Alex Miller (Clojure team)20:09:32

There is no runtime per se, the compiled classes is the whole thing

👀 1
Alex Miller (Clojure team)20:09:49

The repl is literally the functions read eval and print running in a loop

Alex Miller (Clojure team)20:09:30

Read is string -> Clojure data, eval evaluates Clojure data returning more data, and print is Clojure data -> strings

Alex Miller (Clojure team)20:09:05

Eval analyzes, macroexpands, and compiles

hiredman20:09:07

compilation and runtime are not as distinct steps either

Ingy döt Net20:09:21

@U064X3EF3 then calls the compilation unit?

hiredman20:09:10

compilation unit is a term of art in compiles to describe the frontier of what source is visible to the compiler

hiredman20:09:48

so a "compilation unit" isn't actually a thing to call, in some programming languages it might refer to a source file

hiredman20:09:21

in clojure the compilation unit is a single top level expression

Ingy döt Net20:09:59

ok, I thought it was the compiler output, not the input

hiredman20:09:14

in clojure, generally you feed eval a single expression, that expression is compiled to jvm byte code, then that jvm byte code is executed

Ingy döt Net20:09:41

What I meant to ask above of @U064X3EF3 was to clarify that the eval step also executed what it compiled.

hiredman20:09:01

for larger projects you can do things like "aot compilation" which looks similar to file at a time compilation, but is not that

hiredman20:09:35

aot compilation saves the generated byte code, so you can run your clojure program from the bytecode instead of from the clojure source

hiredman20:09:15

the clojure runtime isn't so much running, it is inert code unless called

1
hiredman20:09:45

like part of the clojure runtime is the static method seq on the class clojure.lang.RT

hiredman20:09:19

and some clojure code, when compiled to bytecode and then executed, will reference and possibly call that method

anovick20:09:21

@U064X3EF3 Why do the docs mention both on-the-fly compilation and Ahead-of-time compilation? Is it a choice for the developer which compilation type is used, or do the tools determine which one is best automatically?

hiredman20:09:44

which docs?

hiredman20:09:03

yeah, "on-the-fly" only exists when comparing against aot compilation

hiredman20:09:43

there is the normal clojure modal of compiling to byte code and executing that bytecode as needed

hiredman20:09:43

and there is the aot model, which looks deceptively like batch compilation which you might be familiar with from java, c, haskell, rust, etc

hiredman20:09:02

from the aot model perspective how clojure works looks like "on-the-fly" but outside of looking at it from that perspective it is just how clojure works, no one really refers to it as anything

hiredman20:09:47

the way aot works is basically exactly the same as the normal way clojure works, it just writes the generated bytecode to disk too

hiredman20:09:16

but it is still interleaving compilation and code execution like a repl session would, so you have (launch-the-missiles) as a top level form, when aot compiling you will launch the missiles

hiredman20:09:49

so from a users perspective there is one execution model

hiredman21:09:24

I think that docs page could use a fair bit of improving, it is a mix of the general clojure compilation model and aot related stuff, and if you just want to understand the clojure compilation model, the aot stuff will just confuse you

anovick21:09:57

@U0NCTKEV8 It confuses me that's for sure

hiredman21:09:55

the model is you type in something, clojure reads that in as data structure, then evals the data structure, producing another data structure, then prints the data structure out

hiredman21:09:47

where eval is implemented by compiling the data structure to jvm byte code and executing it

hiredman21:09:53

there are other things clojure.core/compile, load-file, require, ways to launch programs without using a repl, etc but those are all extensions of that model that try to add useful additional things (line numbers in errors, etc) without removing anything from the basic model

hiredman21:09:26

that all being said, a clojure program is not just the repl

hiredman21:09:46

there is no single "repl" for a clojure program

hiredman21:09:18

and there is no single clojure repl, there are two in widespread use the repl built into clojure and nrepl

jpmonettas21:09:33

if you try to look at the compiler code eval is tricky, because for each ast node there is an eval path and a emit path, which is confusing. I would put it more like, for each form you type at the repl : • the LispReader.java will read the string into a nested structure of lists, vectors, maps, symbols etc, called forms • then the parse path in Compiler.java will walk those forms and make Expressions, like (if ...) will make a IfExpr node, etc (this makes the ast) • then the emit path of Compiler will call emit on each of those, creating in memory classes and their bytecode, which don't get written to disk unless you specify it • then those classes are loaded and executed, by calling invoke etc which will generate an output object • then that object is printed to the console

anovick21:09:15

Does every Clojure program have a REPL running?

anovick21:09:55

so it's possible to just have a Clojure code get compiled to a program without a REPL yes?

jpmonettas21:09:42

but every Clojure program can call the reader, and eval with the resulting form, so the compiler is always available

anovick21:09:19

Is the REPL used during compilation?

jpmonettas21:09:51

no, the compilation system doesn't require any repl

hiredman21:09:57

compilation is used during eval, eval is the E in repl

hiredman21:09:56

user=> (defn f [x] x)
#'user/f
user=> (type f)
user$f
user=> f
#object[user$f 0x3eb631b8 "user$f@3eb631b8"]
user=>

hiredman21:09:17

at the repl I typed in (defn f [x] x) which created a function and bound it to f

jpmonettas21:09:18

a repl is just a function you run if you want to interactively call the read->eval(compile,execute)->print

hiredman21:09:36

the function is clojure code compiled to bytecode, and a jvm class

hiredman21:09:52

the type, the name, of that jvm class is given by the type function there

anovick21:09:05

So can we say that by running CLJ code with the clj tool, we have this tool *R*ead all namespaces in the files, *E*valuate them (compile them to bytecode), *P*rint nothing and (not) *L*oop? So basically just a single cycle of interaction yes?

hiredman21:09:55

and this is a key point, it reads form by form in the namespaces and evaluates each as it goes

hiredman21:09:13

which is why it behaves the same way as if you were typing the code into a repl

hiredman21:09:54

and it doesn't "compile" them, it evalutes them, which is compile+execute

anovick21:09:39

so the differences are that compiling the initial code (from a file) is using Ahead-of-time compilation, and saves the bytecode to disk, whereas interacting with the REPL is using on-the-fly compilation, and saves the bytecode only to memory?

seancorfield21:09:28

No, reading from a file is the same form-by-form process as the REPL.

hiredman21:09:43

and reading the file can happen with or without aot compilation

hiredman21:09:12

aot compilation is just like a switch that you turn on and off in the compiler to write the code it generates to disk

hiredman21:09:22

everything else is the same

seancorfield21:09:22

AOT is a "mode" for the "compile" portion of the "eval... yeah, what he said ☝️:skin-tone-2:

hiredman21:09:08

(where everything is subject to some provisos about gen-class, etc)

anovick21:09:11

OK so the underlying mechanism (specific compilation) is just a detail in the evaluation stage?

anovick21:09:13

I could choose to save the compiled bytecode to disk or to memory? whichever I prefer?

anovick21:09:50

And both options are on-the-fly compilation?

seancorfield21:09:45

Typically, folks only use AOT compilation mode as the "last step" when building a deployable artifact. And the sole purpose of it is to make it faster for the application to start up.

anovick21:09:58

Well it makes sense... how would I compile "ahead of time" if I keep interacting with the REPL and sending arbitrary code to it

hiredman21:09:35

generally the interface to aot compilation that clojure provides is a pain, so it isn't used directly, but via tooling

seancorfield21:09:26

You can build an uberjar -- a deployable artifact -- that is all source code and run it via clojure.main:

java -cp path/to/the.jar clojure.main -m my.entry.point
That will run the clojure.main class (the -main function in that namespace) and it will then "require" my.entry.point so Clojure will find my/entry/point.clj on the classpath and load it, which will read and compile and execute each form in the file. If you use "AOT" when building the uberjar, you'll put the compiled-to-disk .class files into the artifact and, if you have a compiled ns with -main in any of that, you can run:
java -jar path/to/the.jar 
(assuming you've also added the manifest describing my.entry.point as the Main-Class) and Clojure will see the .class files on the classpath and load those and skip the compile step. (with a fair bit of hand waving) So the all-source uberjar might take 30 seconds to startup (because Clojure has to compile each piece of code it loads), vs an AOT-compiled uberjar which might take 3 seconds to startup. That's literally the difference.

seancorfield21:09:05

Some people use AOT compilation to help speed up starting their app via the REPL. See https://clojure.org/guides/dev_startup_time

seancorfield21:09:11

(I did it for a while and then decided it really wasn't worth it -- even with as large a codebase as we have at work)

anovick22:09:49

All of this tooling technicalities are confusing for me. I may have learned a thing or two from this discussion. I'll save the rest for another day to hopefully unravel the remaining mysteries eventually.

seancorfield22:09:53

For the vast majority of Clojure-related work, the subtleties of this stuff just don't matter. Many Clojure devs live productive working lives without caring about these details 🙂

anovick22:09:50

@U04V70XH6 Yea, but when I'm interested in developing some dev tool, understanding behind-the-scene workings is sometimes necessary (if my dev tool has to do with the REPL)

anovick22:09:00

referencing that idea I had raised earlier (keeping track of function usage)

seancorfield22:09:35

Sure, but that would operate at the source level -- and for it to be widely useful, I suspect it would need to be nREPL middleware (as well as perhaps something for the "regular" built-in REPL).

anovick22:09:53

Thanks @U04V70XH6 for the thoughtful replies. Very much appreicate it 🙂 have a great day

stopa20:09:25

Hey team, curious question: I am playing with the cel-java library. I see they have a JsonType class: https://github.com/google/cel-java/blob/main/common/src/main/java/dev/cel/common/types/JsonType.java Buut, if I try to import it, I get:

; eval (root-form): (ns instant.db.play.cel (:import (dev.cel.common CelAbst...
; (err) Execution error (ClassNotFoundException) at java.net.URLClassLoader/findClass (URLClassLoader.java:445).
; (err) dev.cel.common.types.JsonType
I can import something like dev.cel.common.types.SimpleType. I may be missing something simple. How would you go about debugging this further?

Bob B21:09:50

I don't see the class in the jar file

stopa21:09:48

Hmm -- okay, maybe they have a bug -- looks like they're actively hacking!

Bob B21:09:53

in the build file it looks like JsonType is in a separate build declaration (or whatever they're called in that build system) - it's not immediately apparent to me how that relates to what goes into the output jar file

stopa21:09:17

Understood. Thank you @U013JFLRFS8!

Sahil Dhanju21:09:51

I'm doing a data transformation using into and transducers which produces a collection col . I then pass col to my function called save! which iterates over col and inserts each element into a db. What's the idiomatic way of making save! be a part of the transduction instead of this extra step?

Sahil Dhanju21:09:35

Is it something like this?

(transduce xf save! init-data)

👍 1
Sahil Dhanju22:09:24

My confusion comes from that there isn't really any result of the transduction, it's all side-effects, so unsure if this is possible.

phronmophobic22:09:36

you can use transduce and just ignore the accumulation value.

phronmophobic22:09:07

another option is: (run! save! (eduction xf1 xf2 xf3 init-data))

Jason Bullers22:09:31

The run! option seems clearer to me. Using transduce for side effects seems like it would be a surprise later on. You'd also need to make save! a reducing function, I think, whereas with run! it's just a command to execute.

👍 1
Sahil Dhanju23:09:32

@U7RJTCH6J quick follow up question: (eduction xf1 xf2 xf3 init-data) - this is returning a type of Eduction. When/How is this evaluated by run!?

Sahil Dhanju23:09:08

Oh right the implementation uses reduce

👍 1
phronmophobic23:09:05

Right, the eduction won't start applying xforms. each time the eduction is passed to a function that expects a seqable or reducible, then it will process the xforms.

phronmophobic23:09:31

and as you said, run! uses reduce under the hood.

Sahil Dhanju23:09:46

I found that during my reduction, I was wanting to refer to previous values or do small little side-effect tasks like logging and the continue with the reduction. I wrote this function to be able to do it

(defn passthrough [f]
    (fn [xf]
        (fn
            ([] (xf))
            ([result] (xf result))
            ([result input]
             (f input)
             (xf result input)))))
This will just call the supplied function and continue on. Is this reasonable? Or is the fact I'm wanting to do these side tasks a smell that I shouldn't be doing a reduction the whole way through?

phronmophobic23:09:31

It would be interesting to hear what others think, but seems totally reasonable to me 👍

phronmophobic23:09:20

I think separating who, what, when, why, and how is a good idea, but I tend to put transducers in the when/how category so adding more when/how steps is reasonable.

phronmophobic23:09:49

There's a lot of similarities between transducers, middleware (eg. ring), and interceptors (eg. pedestal) which also are about managing when/how (eg. side effects).

Sahil Dhanju23:09:21

I haven't heard of interceptors. When you say middleware are you referring to the web definition of it or a broader definition of it?

phronmophobic00:09:50

I guess middleware is an overloaded term. I was more specifically referring to https://github.com/ring-clojure/ring, but there are other libraries that have a similar implementation like https://nrepl.org/nrepl/design/middleware.html.

👍 1
Sahil Dhanju00:09:22

What are your thoughts on this part of what I said earlier? > I was wanting to refer to previous values Hmm now that I think of it that makes way less sense because the shape of the data is arbitrarily changing

👍 1
Sahil Dhanju00:09:35

Oh and specifically I mean - is there a way to refer to values produced by previous steps of a reduction while you're in the reduction? I assume no

phronmophobic00:09:16

I believe referencing previous values is already available via https://github.com/cgrand/xforms window, window-by-time, and reductions.

🤯 1
Sahil Dhanju00:09:11

Will look at that! Thanks again, this was extremely helpful. This is the first time I was really writing transduction code but I've been reading/watching as many resources as I can find

👍 1
huy21:09:43

Is there a way to pretty-print output of hiccup/html? By default, the whole html is on a single line!

rolt08:09:32

it's not optimal but you could use jsoup to parse and reformat the generated html

(println (.toString (org.jsoup.Jsoup/parse "... html ...")))

thanks3 1
huy08:09:45

thank you @U02F0C62TC1, I'll have a look into it.

escherize17:09:03

Did you try clojure.pprint/pprint

huy07:09:31

@U051GFP2V Yes, I have tried:

(-> [:div
       [:h1 "Hello"]
       [:p "World"]]
      (hc/html)
      (str)
      (clojure.pprint/pprint))
"<div><h1>Hello</h1><p>World</p></div>"
=> nil

escherize13:09:37

Oh, right. I thought you were asking to pretty print the hiccup data. e.g.

(require 'clojure.pprint)
(-> [:div [:h1 "Hello"] [:p "World"]
     [:ul (for [n (range 5)]
            [:li [:ol (for [m (range (rand-int 10))]
                        [:p n ">" m])]])]]
      clojure.pprint/pprint)

;; [:div
;;  [:h1 "Hello"]
;;  [:p "World"]
;;  [:ul
;;   ([:li [:ol ([:p 0 ">" 0] [:p 0 ">" 1] [:p 0 ">" 2])]]
;;    [:li [:ol ([:p 1 ">" 0] [:p 1 ">" 1])]]
;;    [:li [:ol ([:p 2 ">" 0] [:p 2 ">" 1] [:p 2 ">" 2] [:p 2 ">" 3])]]
;;    [:li [:ol ([:p 3 ">" 0])]]
;;    [:li
;;     [:ol
;;      ([:p 4 ">" 0]
;;       [:p 4 ">" 1]
;;       [:p 4 ">" 2]
;;       [:p 4 ">" 3]
;;       [:p 4 ">" 4])]])]]