Fork me on GitHub
#beginners
<
2024-06-07
>
3starblaze14:06:41

Hey, I have functions in namespace that rely on 2 globals to be set, in order to work correctly. How would you recommend handling this situation? Currently I have an init function that must be called before the library can be used and if the user tries to call the libraries before initialization, they get an Exception. For the context, I receive a global (p-get-proc-address) via native library. This is a fragment of the current solution. It does work but perhaps my current solution would bring headaches later on.

(def init-state nil)

(defn init! [p-get-proc-address]
  (alter-var-root
   #'init-state
   (constantly
    {:p-get-proc-address (Function/getFunction (Pointer. p-get-proc-address))
     :gd-extension-type-registry (ast-utils/import-gd-extension-type-registry!)})))

Ed14:06:23

Depending on what behaviour you would like the readers of the globals to be, you could use a different construct. Clojure provides a promise that you can deliver. Maybe something like

(def init-state (promise))

(defn init! []
  (deliver init-state :value))

(defn do-work []
  (str "Tada! " @init-state))

(comment

  (init!)
  (do-work)

  )
and if you call (do-work) before (init!) it will block indefinitely until the promise has been delivered, presumably by another thread. There is also a version of @/`deref` that takes a timeout (deref init-state 1000 :timeout) so you can write some custom logic if the timeout is reached, and maybe throw an exception.

Ed14:06:42

Another option might be to just use a delay to initialise the state. This would run the initialisation code once on first deref and mean you don't have to call (init!)

(def init-state (delay (do
                         (prn '>> 'init-state)
                         {:something (+ 1 2 3)})))

(defn do-work []
  (str "Tada! " @init-state))

(comment

  (do-work)

  )

3starblaze14:06:23

Blocking would not be a good experience for the users and specifying the timeout every time I use the value internally would not be ergonomic. Though, I could set a default value to something that derefs to a thrown exception.

3starblaze14:06:36

delay would not work in my case because the native library passes a pointer that cannot be determined beforehand

Ed14:06:24

you could also memoize the function?

3starblaze14:06:54

Isn't that the same idea as with delay?

Ed14:06:08

but that would turn it into a lookup, essentially. Meaning users would need to have a reference to the pointer.

Ed14:06:37

... I didn't explain my idea very well. I meant write a function that turns p-get-proc-address into the data structure you need, and memoize that

Ed14:06:03

the blocking would only happen first time on the promise. I guess that you could write a wrapper function that did the deref + timeout + exception throwing for them?

Ed14:06:36

I guess it very much depends on what you mean by "ergonomic" ๐Ÿ˜‰

3starblaze14:06:26

My criteria: - the users shouldn't be worried about that value's existence - it isn't cumbersome to manage in code - is testable I think the promise idea is the best idea at the moment, though, instead of using promises, I could set a function that throws exception on deref.

Ed14:06:10

I think the "best" thing to do is often to not have global state, and make each function declare it's dependencies. I think that means rejecting your first criteria. The users do care about the value that they get from derefing the promise, or however they get that value. The semantics of what they do if that value isn't available, will help you choose a construct.

delaguardo14:06:58

But why globals? it is in general not the best idea to ship a library that requires user to "initialize" some global state

Ed14:06:10

it might be easiest to separate out all the pieces into different functions and then orchestrate them at the end.

Ed14:06:27

> it is in general not the best idea to ship a library I'm not sure that the question is about shipping a library. It could be just be an application.

3starblaze14:06:38

I understand that global variables are to be avoided but I believe there is no way around it. I am making godot-clojure, a library that lets users call Godot functions via Clojure and that interop has to go through C and that requires me to have a global handle that I can use to call the corresponding C functions.

delaguardo14:06:30

can your library initialize globals itself? without a request from the user

Ed14:06:13

I suggest creating a "context" object (with a constructor function) that you pass to each of the functions that require c interrop. Then it's up to the consumer of the lib to manage that state in whatever way makes most sense to them.

โž• 1
3starblaze14:06:39

> can your library initialize globals itself? without a request from the user no because I need a function pointer from Godot which is only known when Godot starts running

delaguardo14:06:47

so the user starts godot process and after that can call api functions, right?

3starblaze14:06:53

yes, exactly. The idea is that the user defines a hook which is the first thing that Godot calls and the low-level details are handled behind the scenes

Ed15:06:41

> the users shouldn't be worried about that value's existence this sounds very much in conflict with > the user starts godot process and after that can call api functions Could the process be: 1. user stats godot process 2. user constructs context obj 3. user calls lib functions using context obj as param Then the state is explicitly handled by whatever mechanism the user wants?

Ed15:06:17

and if it falls over or fails, they're in charge of how it restarts and so-on?

delaguardo15:06:15

instead of exposing top-level functions I would expose a protocol with an implementation that wraps p-get-proc-address as far as I can tell, your lib exposes call! and unsafe-call! public functions. They can become a part of that protocol.

3starblaze15:06:32

Passing around that state could be a solution although that would mean that the user has to bring it everywhere because almost all useful functions would require the state. If the context changes then that would mean Godot would have restarted and spawned a new Clojure process. Protocol and passing around the context might be the best option, thanks for all the help!

3starblaze15:06:07

(also call! and unsafe-call! are low-level functions and the "real" api would be higher-level but the idea stays the same)

sheluchin16:06:51

How do I debug an error like this?

; (err) Execution error (ClassCastException) at tech.v3.datatype.ffi/make-ptr (ffi.clj:587).
; (err) class tech.v3.datatype.native_buffer.NativeBuffer cannot be cast to class tech.v3.datatype.native_buffer.NativeBuffer (tech.v3.datatype.native_buffer.NativeBuffer is in unnamed module of loader clojure.lang.DynamicClassLoader @380b1b0; tech.v3.datatype.native_buffer.NativeBuffer is in unnamed module of loader clojure.lang.DynamicClassLoader @501b44ed)

hiredman17:06:06

it means you need to restart your repl

sheluchin17:06:41

I tried, same result.

hiredman17:06:26

tech.v3.datatype.native_buffer.NativeBuffer cannot be cast to class tech.v3.datatype.native_buffer.NativeBuffer is saying a classes with the same name are not actually the same class, which happens because you loaded some code that defined that class, made an instance of the class, then reloaded the code that defines the class got a new class with the same name, then tried to use the old instance with code compiled to expect the newly defined class

hiredman17:06:57

what does "tried" mean?

hiredman17:06:12

what are you doing that results in that error?

hiredman17:06:43

do you have a user.clj? are you using and kind of auto code reloading? are you calling clojure.core/compile some where?

hiredman17:06:08

could maybe also be a stale aot class file issue, where the class is being imported somewhere, but the clojure code that defines the class is not being loaded first, so that import gets an old version of the class from disk, then the clojure code is loaded and it defines the class again

sheluchin17:06:26

> what does "tried" mean? I did what you suggested and it didn't fix the issue. > what are you doing that results in that error? It happens when I call the open-db fn here:

(ns com.example.rad.components.database
  (:require
    ; [com.fulcrologic.rad.database-adapters.datomic-cloud :as datomic]
    [mount.core :refer [defstate]]
    [com.example.rad.model-rad.attributes :refer [all-attributes]]
    [com.example.rad.components.config :refer [config]]
    [tmducken.duckdb :as duckdb]))

(comment
  ;; FIXME: move paths n stuff to config
  (duckdb/initialize! {:duckdb-home "/tmp/tmducken/binaries/"}) ; true
  (def db (duckdb/open-db "/tmp/duck.db"))
  (def conn (duckdb/connect db)))
My user.clj looks like this:
(ns user
  (:require
    [clojure.pprint :refer [pprint]]
    [clojure.spec.alpha :as s]
    [clojure.tools.namespace.repl
     :as tools-ns
     :refer [disable-reload! refresh clear set-refresh-dirs]]
    [expound.alpha :as expound]
    [portal.api :as portal]
    [taoensso.timbre :as log]))

(alter-var-root #'s/*explain-out* (constantly expound/printer))

(def p (portal/open {:app true
                     :theme :portal.colors/material-ui}))
(add-tap portal/submit)
> are you calling clojure.core/compile some where? No.

sheluchin17:06:42

I notice I also get these warnings when starting the repl:

; Assumed session: Toyger (Unknown )
; (err) Reflection warning, tech/v3/dataset/io.clj:60:3 - reference to field close can't be resolved.
; (err) Reflection warning, tech/v3/dataset/io.clj:60:3 - reference to field close can't be resolved.
; (err) Reflection warning, tech/v3/dataset/io.clj:60:3 - reference to field close can't be resolved.
nil

hiredman17:06:13

I dunno that I have the patience to try and untangle the techascent libraries to figure out where NativeBuffer is defined, but the issue is ultimately that the code that defines it is being run twice, resulting in to incompatible definitions

hiredman17:06:08

there are many ways this can happen, the two big ones are some kind of auto-reload tooling or aot compilation

hiredman17:06:27

(by incompatible definitions I just mean you end up with two classes with the same name, which is possible on the jvm because class identity is name+classloader)

hiredman17:06:28

ah, it comes from https://github.com/cnuernber/dtype-next not under the techascent github

sheluchin17:06:05

What is the significance of that finding, @U0NCTKEV8?

hiredman17:06:39

if you don't know where the code is coming from how can you read it?

hiredman17:06:18

if you add a require of tech.v3.datatype.native-buffer it might make the error go away

hiredman17:06:56

(as early as you can before any other code that might use it is loaded)

sheluchin17:06:41

Nope, same deal. I'll make a minimal repro to isolate it.

sheluchin17:06:40

@U0NCTKEV8 requiring tech.v3.dataset before tmducken.duckdb seems to have fixed it! The example in https://github.com/techascent/tmducken does it in that order but it wasn't obvious to me that it's required.

hiredman17:06:40

it shouldn't be required, it is a symptom of something being very broken, and a hack to get around it

hiredman17:06:00

something in the dependency tree may be aot compiled (a problem for libraries) which generates a class file for NativeBuffer, and then something else is importing it without requiring the clojure namespace that defines it

sheluchin17:06:18

Weird, I could not reproduce in a minimal repro; I was able to just require tmducken.duckdb without tech.v3.dataset. Could this be a case of when blowing away my .m2 might just magically fix it?

hiredman17:06:24

how are your deps specified? project.clj, deps.edn?

hiredman17:06:38

if you are using deps.edn and on linux something like clj -Spath | sed 's/:/\n/g'|grep jar|xargs -n1 jar -tf|grep NativeBuffer will tell you if some dependency has a NativeBuffer.class somewhere

sheluchin18:06:58

@U0NCTKEV8

$ clj -Spath | sed 's/:/\n/g'|grep jar|xargs -n1 jar -tf|grep NativeBuffer

tech/v3/datatype/NativeBufferData.class

hiredman18:06:08

you can add something like (alter-var-root #'clojure.core/*loading-verbosely* (constantly true)) before your ns form, which will cause it to print out all the namespaces as they are loaded, and you might see tech.v3.datatype.native-buffer loaded twice

hiredman18:06:10

yeah, looks like something is causing tech.v3.* namespaces to get loaded over and over

hiredman18:06:16

that'll break stuff

sheluchin18:06:18

Looks like native-buffer appears ~900 times in that output in these three variants:

; (out) (clojure.core/alias 'native-buffer 'tech.v3.datatype.native-buffer)
; (out) (clojure.core/alias 'nbuf 'tech.v3.datatype.native-buffer)
; (out) (clojure.core/in-ns 'tech.v3.datatype.native-buffer)

hiredman18:06:28

the aliases are what matters, the lines that matter are like ; (out) (clojure.core/load "/tech/v3/datatype/native_buffer")

hiredman18:06:40

which happens maybe 8 times

sheluchin18:06:13

Is this somehow my fault or is it something that should be reported to the tmducken repo?

hiredman18:06:10

seems unlikely to be your fault, but it could be some tooling you are using, you should try and replicate without nrepl and cider and without the user.clj

hiredman18:06:30

since it seems to be effecting text.v3.* namespaces only, it makes me wonder if they have some stray :reload-all directives floating around

sheluchin18:06:02

$ clj
Clojure 1.10.3
user=> (clojure.core/require 'com.example.rad.components.database)
Reflection warning, tech/v3/dataset/io.clj:60:3 - reference to field close can't be resolved.
Reflection warning, tech/v3/dataset/io.clj:60:3 - reference to field close can't be resolved.
Reflection warning, tech/v3/dataset/io.clj:60:3 - reference to field close can't be resolved.
2024-06-07T18:33:32.001Z xps INFO [tmducken.duckdb:326] - Attempting to load duckdb from "/tmp/tmducken/binaries/libduckdb.so"
Execution error (ClassCastException) at tech.v3.datatype.ffi/make-ptr (ffi.clj:587).
class tech.v3.datatype.native_buffer.NativeBuffer cannot be cast to class tech.v3.datatype.native_buffer.NativeBuffer (tech.v3.datatype.native_buffer.NativeBuffer is in unnamed module of loader clojure.lang.DynamicClassLoader @598464b2; tech.v3.datatype.native_buffer.NativeBuffer is in unnamed module of loader clojure.lang.DynamicClassLoader @5fc44b4b)
user=> 

hiredman18:06:17

is that without the user.clj aswell?

sheluchin18:06:43

Yes, it is. I was able to reproduce in a new project as well, but it turns out that upgrading from clojure 1.10.3 -> 1.11.1 made the problem go away ๐Ÿ˜•

sheluchin18:06:29

I just started commenting out deps in deps.edn and when I got to org.clojure/clojure {:mvn/version "1.10.3"} so it defaulted to my local 1.11.1, I saw the problem was gone.

hiredman18:06:53

๐Ÿคท

sheluchin18:06:41

Thanks very much for your patience and help, @U0NCTKEV8. I noted your suggestions to try *loading-verbosely* and -Spath parsing to review in more detail later.

Jason Bullers17:06:04

What's the difference between set! and alter-var-root? E.g. (set! *warn-on-reflection* true) and (alter-var-root *warn-on-reflection* (constantly true))

๐Ÿ‘ 1
Alex Miller (Clojure team)17:06:20

dynamic vars have a global root value and a per-thread override

Alex Miller (Clojure team)17:06:33

set! changes this thread's binding

Alex Miller (Clojure team)17:06:40

alter-var-root changes the global root

๐Ÿ‘ 1
Jason Bullers17:06:03

Ah, I see. Thanks ๐Ÿ‘:skin-tone-2:

Alex Miller (Clojure team)17:06:33

the other non-obvious thing is that set! only works if binding has been used in the current thread context to set a per-thread value. The repl itself sets a bunch of core dynamic vars before control is handed to you, which is why you can (set! *warn-on-reflection* true) but may not be able to set! other dynamic vars

๐Ÿ‘ 1
Jason Bullers17:06:28

If binding hasn't happened, then would alter-var-root be the only way to replace the value of a dynamic var with something else? Of course assuming it's correct for that value to apply across all threads.

Alex Miller (Clojure team)17:06:57

you can use binding of course

Alex Miller (Clojure team)17:06:20

vars have dynamic per-thread scope - using binding will define that dynamic scope as "inside this block and anything it calls"

Alex Miller (Clojure team)17:06:12

so you can (binding [*warn-on-reflection* true] (do-some-stuff)) and reflection will be checked in everything called by do-some-stuff

Alex Miller (Clojure team)17:06:38

also, some multi-threaded contexts (like future) convey the current thread bindings to the executing thread

Rushal Verma17:06:11

I am new with clojure and just started using calva repl in vscode with Leiningen this is my core.clj

(ns clojure-noob.core
  (:gen-class))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "I'm a little teapot and handling the calva too!"))


(+ 1 23124 3)

(let [name "Rushal"]
  (str "Hello " name))

(defn add [a b]
  (+ a b))

(add 1 2)
All of the above function were running fine except the last one which produces the stacktrace. The stacktrace was
clj๊ž‰clojure-noob.core๊ž‰> 
; Syntax error compiling at (src/clojure_noob/core.clj:18:1).
; Unable to resolve symbol: add in this context
clj๊ž‰clojure-noob.core๊ž‰> 
clojure.lang.Compiler/analyze (Compiler.java:6825)
clojure.lang.Compiler$InvokeExpr/parse (Compiler.java:3832)
clojure.lang.Compiler/analyzeSeq (Compiler.java:7126)
clojure.lang.Compiler/analyze (Compiler.java:6806)
clojure.lang.Compiler$BodyExpr$Parser/parse (Compiler.java:6137)
clojure.lang.Compiler$FnMethod/parse (Compiler.java:5479)
clojure.lang.Compiler$FnExpr/parse (Compiler.java:4041)
clojure.lang.Compiler/analyzeSeq (Compiler.java:7122)
clojure.lang.Compiler/analyze (Compiler.java:6806)
clojure.lang.Compiler/eval (Compiler.java:7191)
clojure.core/eval (core.clj:3215)
clojure.core/eval (core.clj:3211)
nrepl.middleware.interruptible-eval/evaluate (interruptible_eval.clj:87)
clojure.core/apply (core.clj:667)
clojure.core/with-bindings* (core.clj:1990)
nrepl.middleware.interruptible-eval/evaluate (interruptible_eval.clj:87)
clojure.main/repl (main.clj:437)
clojure.main/repl (main.clj:458)
nrepl.middleware.interruptible-eval/evaluate (interruptible_eval.clj:115)
nrepl.middleware.interruptible-eval/interruptible-eval (interruptible_eval.clj:150)
nrepl.middleware.session/session-exec (session.clj:218)
nrepl.middleware.session/session-exec (session.clj:217)
java.lang.Thread/run (Thread.java:1570)
whatโ€™s the issue here and why Iโ€™m getting the stacktrace?

seancorfield17:06:12

Most likely you tried to eval (add 1 2) without eval'ing (defn add ...)

๐Ÿ™Œ 1
Rushal Verma17:06:36

๐Ÿ˜ฎ ohh thanks; it worked, I thought the calva repl actually runs from the top also, how do you get that idea that I didnโ€™t evaluated the (defn add โ€ฆ) from the stacktrace? How can I read that in this scenario?

seancorfield18:06:56

The error Unable to resolve symbol: add in this context indicates the running REPL doesn't know about add so it seemed likely that you hadn't eval'd the defn form.

โœ… 1
seancorfield18:06:12

My recommendation is to eval each top-level form as you write it or change it -- alt+enter (Windows/Linux) opt+enter (macOS) -- and just get into the habit of doing that whenever you make any changes.

๐Ÿ™Œ 1
Rushal Verma18:06:39

is there a way by which I can eval every statement in the file? Run all kind of thing?

seancorfield18:06:34

ctrl+alt+c enter (`ctrl+opt+c enter` on macOS) will (re)eval the whole file. You could also use ctrl+shift+alt+enter (`ctrl+shift+opt+enter` on macOS) to eval from start-of-file to cursor -- be aware that will close brackets at the cursor if you're not at the end of a top-level form.

seancorfield18:06:57

I often work for extended periods without even saving files, and just press alt+enter any time I make any change, even across multiple files.

๐Ÿ™Œ 1
Rushal Verma18:06:48

Thanks, this helped a lot ๐Ÿ™

Rushal Verma18:06:59

Iโ€™ll follow the same

pez18:06:17

Thereโ€™s a command in Calva that starts a small REPL tutorial, @U076UKX1ACQ. Calva: Fire up the Getting Started REPL. Just FYI. I think I will add something about understanding that message Unable to resolve symbol: foo in this context. It has gotta be something a lot of beginners face.

James Amberger21:06:38

anyone have good way to print all vars and fns defined in my src? am I being dumb and grep is the answer?

respatialized21:06:27

ns-publics does this for public vars (which would include functions too), but only in a single namespace

phronmophobic21:06:18

There's a bunch of ways to do this. You can use something like codox to create a fancy webpage. You could use clj-kondo's static analysis to get the data for programmatic usage. You could use https://github.com/borkdude/grasp. Grep works. As @UFTRLDZEW suggested, you could use the various tools available at runtime.

sharky22:06:18

Can I use swap with a threading macro ?

sharky22:06:30

I need to change multiple values in a map

sharky22:06:59

let's say the map is an atom

sharky22:06:39

The first "change" works , but on the second swap operation I got

Bob B22:06:12

please use a thread instead of separate messages for one question... As long as the arg to swap! is a function, it should be doable to use a threading macro. It would probably help to boil down a self-contained example and share some code so it's clearer what you mean by 'use x with y'

hiredman23:06:23

I would be extremely skeptical of multiple swap! calls in a row on a single atom

hiredman23:06:19

Very likely introducing a possible race condition

sharky22:06:55

class clojure.lang.PersistentArrayMap cannot be cast to class clojure.lang.IAtom (clojure.lang.PersistentArrayMap and clojure.lang.IAtom are in unnamed module of loader 'app')

phronmophobic22:06:44

You'll want to combine all the changes into a single function. (swap atm #(-> % <changes>))

phronmophobic22:06:38

(let [atm (atom {:foo 42})]
  (swap! atm
         #(-> %
              (assoc :bar "bar"
                     :baz :baz)
              (update :foo inc))))
;; {:foo 43, :bar "bar", :baz :baz}

๐Ÿ™ 1
sharky22:06:25

Ah ok , I have multiple swaps ๐Ÿ™‚ . Let me try...

phronmophobic22:06:52

One of the fun parts of learning clojure is that you realize that you can write the same programs with dramatically less state. One trick is combining multiple changes that should happen at the "same time". Another is that you often find that you didn't need the atom in the first place.

sharky23:06:17

Sure , but the hard part for me is that is just give you a compilation error and that's it .

sharky23:06:41

but thank you , I need to rewrite my functions and see if it helps ๐Ÿ™‚

phronmophobic23:06:05

That's fair. I think something that really helps is having a good setup that let's you write and run your program in small pieces (ie. repl driven development). It's possible you're already setup, but it can be a big shift depending on what development setup you're used to.

seancorfield23:06:50

e.g,. Calva has an option to eval the current form to the cursor position, adding closing parens as needed. This lets you take a threading form and put the cursor after each successive subform in the threading pipeline and eval to that position. That makes it a lot easier to debug things like this.

sharky23:06:35

It works

๐ŸŽ‰ 1
seancorfield23:06:59

(-> (swap! my-atom ...)| (swap! my-atom ...) (swap! my-atom ...)) where | is the cursor and ctrl+alt+enter will eval (-> (swap! my-atom ...)) - then move the cursor to after the next swap! call and ctrl+alt+enter will give you that failure. But you'll see that the value of the first eval is a hash map and not an atom, which is the hint.

sharky23:06:56

I am getting better with the brackets . This problem was syntax related I just used the threading macro and called two functions . Each function had a swap statement

sharky23:06:39

Now I removed the functions and just call assoc-in two times and it works ๐Ÿ™‚ , but I haven't seen this syntax

seancorfield23:06:43

Since you didn't post your original code, we're just guessing based on the error you posted ๐Ÿ™‚

seancorfield23:06:42

And both of us are suggesting techniques to help you debug this sort of error on your own -- eval'ing small pieces of code into the REPL so you can see how a larger expression is working (or not).

sharky23:06:06

Thats is what I also try to do . Check if the smallest peace of your function works and then go to the next layer .