Fork me on GitHub
#clojure
<
2024-06-11
>
stagmoose11:06:30

If you are going to implement dfs, how will you do that? If i want to use loop ... recur ... , I haven't come up with a way to implement it without using stack (using stack: https://dnaeon.github.io/graphs-and-clojure/ http://hueypetersen.com/posts/2013/06/25/graph-traversal-with-clojure/) If i don't use stack, this is my version, do you have any suggestions? this is graph example is from pic1

(def graph {0 [1 2 3]
              1 [4 5]
              2 [6]
              3 [7]
              4 []
              5 []
              6 []
              7 []})

  (defn dfs [graph root visited order]
    (if (visited root)
      []
      (into [] (reduce concat [root] (for [v (get graph root)]
                                       (dfs graph v (conj visited root) (conj order root)))))))

  (dfs graph 0 #{} [])
  ;; => [0 1 4 5 2 6 3 7]
My current feeling is that loop recur can be only for "single branching recursion" like graph1 in pic2 but not for "multiple branching recursion" like graph2 in pic2 because i cannot put recur inside for . Is that correct? Thanks for your help!

p-himik11:06:44

You can use loop for graph2 by explicitly calling the same function in the loop. Also, there's nothing wrong with using an explicit stack in a loop. Conceptually it's not different from recursion.

stagmoose12:06:12

@U2FRKM4TW thanks for your reply! do you mean something like this:

(defn func [...]
  (loop [...]
    (for [...]
      (func [...])) ;; probably bind this `for` to some symbol ...
    (recur [...])))

p-himik12:06:12

I'll use this opportunity to mention #C053AK3F9. ;) That's not what I meant. for is lazy - the code will not actually call func. I meant something more similar to

(defn traverse [graph root]
  (loop [children (graph root) ;; Maps are callable.
         result [root]]
    (if-let [[child & children] (seq children)]
      (recur children
             (into result (traverse graph child)))
      result)))
But can be made even simpler:
(defn traverse2 [graph root]
  (into [root]
        (mapcat (fn [child]
                  (traverse2 graph child)))
        (graph root)))

馃檹 1
stagmoose13:06:18

thanks for your help!

馃憤 1
madstap17:06:51

Somewhat related, if I want to do dfs in actual code, I usually reach for tree-seq .

(tree-seq graph graph 0) ;=> (0 1 4 5 2 6 3 7)

馃憤 1
馃憣 1
馃檹 1
stagmoose03:06:53

this is my take after reading: https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core.clj#L4956

(defn dfs [graph root]
    (let [children (graph root)]
      (cons root (mapcat #(dfs graph %) children))))
this is really compact and cool! btw, i can't imagine dfs being a beginner-level problem in other languages haha.

stefanvstein08:06:23

Very Nice. I haven't looked carefully, but doesn't that actually build stack? Look at the lazy-seq in tree-seq.

stagmoose13:06:41

I don't think this is similar to what people usually do when using stack to dfs walk a tree using stack will involve push/pop to a vec or something similar.

p-himik13:06:52

Stefan meant the call stack. Unless recur or lazy-seq is used somewhere (the latter can be used by something internally) and in the right manner, recursion will be increasing the call stack size, eventually leading to a stack overflow error.

p-himik13:06:36

tree-seq uses lazy-seq internally and in the right way, so it won't lead to that error.

stagmoose13:06:25

gotcha! thank for your clarification!

stagmoose13:06:41

i think other langs like cpp or python use this style (building call stack) when doing dfs (if using recursive style instead of pushin/poping to stack data structure)

stefanvstein13:06:48

Yes I mean the call stack. Many of the sequence functions in clojure.core are learning examples. Avoid the ones that mention chunked-seq. Perhaps take-while

馃檹 1
skuro11:06:56

speaking of loop, is there any particular reason why loop seems to make Long instances out of Integers ?

% clj
Clojure 1.11.3

user=> (let [x (int 1)] (prn (class x)))
java.lang.Integer

user=> (loop [x (int 1)] (prn (class x)))
java.lang.Long

oyakushev11:06:35

Clojure in most places only supports double and long primitives. loop is one of such places, loop variables cannot be ints.

o位v13:06:45

Are there any easy to use drop-in replacement for memoize which persists to disk? Performance or efficiency is not important.

adham13:06:43

By "persists to disk" do you mean it remains if you shut down the application and re-run it would still hit a cache next time it runs?

o位v13:06:12

I could just dump the atom to disk

adham13:06:51

Since I don't exactly know what kind of data we are dealing with, you could do that but you can also use https://github.com/clojure/core.cache to have more robust options than just memoize

o位v13:06:18

I might need to try that

adham13:06:40

You could setup a shutdown hook to write to disk before the VM is killed

o位v13:06:46

Ideally I鈥檇 want something quick and hacky, since I鈥檓 just doing some ad-hoc LLM work.

o位v13:06:26

Oh wow, I鈥檝e never heard of shutdown hooks, that sounds perfect!

adham13:06:15

If it is hacky, spit and slurp will be fine and just replace the atom that would be empty upon start

o位v13:06:09

Yeah, that鈥檚 probably good enough.

o位v13:06:10

I鈥檒l try replacing the memoize atom with a https://github.com/jimpil/duratom.

adham13:06:28

interesting, never used duratom before

adham13:06:56

As for the most hacky solution

user> (require '[clojure.core.cache.wrapped :as w])
nil                                                
user> (let [data-atom (w/ttl-cache-factory {})]    
        (swap! data-atom (fn [a]                   
                           (assoc a                
                                  :key             
                                  "value")))       
        (spit "foo.txt" @data-atom))     ;; Do this during the shutdown hook          
nil                                                
user> (read-string (slurp "foo.txt"))              
{:key "value"}                                     
This is probably it

馃憤 1
adham13:06:29

This does not fit your requirement for replacing memoize in place unfortunately

p-himik14:06:08

A slight improvement of the above - swapping and deref'ing in two separate instructions creates a race condition, but swap! returns the swapped-in value so you can (->> (swap! ...) (spit ...)).

daveliepmann15:06:10

The alternatives already mentioned seem viable, but fwiw when I had the same requirement I wrote a couple lines on top of https://github.com/jackrusher/spicerack and it was very nice. Not quite "drop-in replacement" but very very close.

o位v16:06:52

Here鈥檚 what I ended up with:

(defn durmemoize
  "duratom + memoize = 鉂わ笍"
  {:added "1.0"
   :static true}
  [f & args]
  (let [mem (apply duratom args)]
    (fn [& args]
      (if-let [e (find @mem args)]
        (val e)
        (let [ret (apply f args)]
          (swap! mem assoc args ret)
          ret)))))

(defonce quick-embed
  (durmemoize
   (fn [text]
     (-> (cai/create-embedding {:model "text-embedding-3-small"
                                :input text})
         (get-in [:data 0 :embedding])))
   :local-file
   :file-path "quick-embed.duratom"))

o位v16:06:47

And to extract the map from the original memoize:

@(let [field (.getDeclaredField (class quick-embed) "mem")]
          (.setAccessible field true)
          (.get field quick-embed))

o位v16:06:12

It鈥檚 amazing that I can install a library, surgically extract state from a closure and switch to a durable memoization all at runtime. Really happy about not losing my memoization state :^).

p-himik16:06:14

You shouldn't use .getDeclaredField - that's an implementation detail that mem ends up being a field on the class. A better way would be to add metadata to the returned function.

o位v17:06:34

@U2FRKM4TW Converting to a durable memoization is a one-time change so it鈥檚 fine :^) How else would I extract the memoization data from a memoized function at runtime?

o位v17:06:21

(memoize fn) doesn鈥檛 expose its state so I don鈥檛 think there鈥檚 any way around it.

p-himik18:06:05

> How else would I extract the memoization data from a memoized function at runtime? As I said - via function metadata. Here's how core.memoize does it:

(defn- cached-function
  "..."
  [f cache-atom ckey-fn]
  (with-meta
   (fn [& args]
     ...)
   {::cache cache-atom
    ::original f}))
Not everyone is a fan of using metadata on functions (should be easy enough to find previous discussions here on the topic), but I'd say it's still better than using a reflection to rely on an implementation detail.

o位v19:06:16

That鈥檚 great! In my case the problem was that there was state inside of a live memoized function that I wanted to extract. The fact that if I had memoized it differently I could鈥檝e retrieved it via metadata, doesn鈥檛 really help me with that.

p-himik19:06:25

I don't understand what you mean. Here's how the code above that you ended up with can be modified to use metadata:

(defn durmemoize
  "duratom + memoize = 鉂わ笍"
  {:added  "1.0"
   :static true}
  [f & args]
  (let [mem (apply duratom args)]
    (with-meta
      (fn [& args]
        (if-let [e (find @mem args)]
          (val e)
          (let [ret (apply f args)]
            (swap! mem assoc args ret)
            ret)))
      {:mem mem})))

(def quick-embed (durmemoize unmemoized-quick-embed ...)

@(:mem (meta quick-embed))

馃憤 1
Anton Shastun13:06:35

Do you know how to suppress reflection warnings:

Reflection warning, meander/util/epsilon.cljc:758:24 - reference to field val can't be resolved.
i have added:
(set! clojure.core/*warn-on-reflection* false)
to user.clj but did鈥檛 helped

adham13:06:01

Could you try (set! *warn-on-reflection* false) without the fully qualified name?

馃憤 1
p-himik13:06:58

That set! works only at the namespace level. In this case, you cannot override it at all because it's used at the source level: https://github.com/noprompt/meander/blob/epsilon/src/meander/util/epsilon.cljc#L12 You should create an issue for Meander to get the warning fixed.

p-himik13:06:35

Or maybe you're using an old version of Meander: https://github.com/noprompt/meander/issues/230

p-himik13:06:07

If, as the second comment there states, there has been no new release since the fix, you can probably use the Git coordinate.

Anton Shastun13:06:53

@U03QTHYKXK7 thank you, that works!

p-himik14:06:52

Hmm, I wouldn't have expected for that to work to be honest. But I've been so disenchanted by user.clj so I've probably forgotten how it works.

p-himik14:06:00

Just in case - have you restarted the REPL after making the change and before checking? Even if you have that set! in user.clj, the set! in meander.util.epsilon should override it.

Anton Shastun14:06:02

@U2FRKM4TW thank you! have鈥檛 know about

That set! works only at the namespace level. 

Anton Shastun14:06:03

actually yes:face_palm: after reload meander its spaming with warn again

p-himik14:06:08

*warn-on-reflection* and clojure.core/*warn-on-reflection* are exactly the same. :) The former gets resolved to the latter.

Anton Shastun14:06:35

@U2FRKM4TW do you know any other way how to off reflections warn?

p-himik14:06:28

As I mentioned, by using the Git coordinate of Meander. That's what I would do.

Anton Shastun14:06:09

Do you mean use latest from master?

p-himik14:06:06

Maybe not the latest, but after that fix mentioned in the issue. Up to you, I haven't checked what the actual most recent commits are.

stopa23:06:36

Curious question: In future-call, we use binding-conveyer-fn to convey thread bindings: https://github.com/clojure/clojure/blob/clojure-1.11.1/src/clj/clojure/core.clj#L7038-L7040 It seems like binding-conveyer-fn has the same purpose as bound-fn* Is there a reason binding-conveyer-fn was chosen in future-call, rather than bound-fn*? I asked because I have a similar future-call function in my code. I originally copied binding-conveyer-fn, but @hiredman pointed me to the nifty bound-fn*. I am not sure if there were tradeoffs for one vs the other

hiredman00:06:52

binding-convey-fn is not safe to use on a threadpool that might mix binding conveying work and non-conveying work

hiredman00:06:44

binding-conveyor-fn just blindly splats bindings in and doesn't remove them when done

stopa16:06:18

Ah, good to know! Just found this too, which says binding-conveyer-fn may lead to memory leaks too. Good to know! https://clojurians.slack.com/archives/C06E3HYPR/p1584642408134800 Thanks @hiredman