Fork me on GitHub
#beginners
<
2022-04-04
>
Benjamin08:04:22

should I use letfn over let [x (fn.. ?

dharrigan08:04:41

imo, just a matter of personal choice.

tomd08:04:03

Probably not. 2 main reasons to use are mutual recursion and needing the fns to be named (e.g. for stack traces) without confusing duplication of the names. Otherwise, the syntax is a bit jarring, so may inappropriately imply "hey look here, something a bit unusual is going on" when it probably isn't.

🙌 1
delaguardo12:04:09

to get nicer traces you can "name" anonymous functions as well 🙂

(let [x (fn x [] (throw (ex-info "!!!" {})))
      y (fn y [] (x))]
  (y))

1. Unhandled clojure.lang.ExceptionInfo
   !!!
   {}
                      REPL:    1  bundle.core/eval111728/x
                      REPL:    2  bundle.core/eval111728/y
                      REPL:    3  bundle.core/eval111728 
note x and y at the ends of lines 1 and 2

💯 1
delaguardo12:04:02

so that shrinks the list of reasons to use letfn to only one occasion: mutual recursion

Sam Ritchie15:04:08

I like it for better indentation in that named function case

1
👍 1
kennytilton15:04:28

I use letfn only in coding exercises for jobs so it opens the door to me explaining how silly it was for them to require coding exercises. They usually agree. hth.

teodorlu16:04:11

How would you write todo?

(defn todo [m])

;; such that

(= (todo
    {:a 99
     :x {:b 999
         :y {:z 123
             :w 456}}})
   [[:a] 99
    [:x :b] 999
    [:x :y :z] 123
    [:x :y :w] 456])

oddsor17:04:48

I feel like I can make something more elegant than this, but for starters we can do a recursive function that is provided “the path so far”:

(defn pathify 
  ([m path]
   (mapcat (fn [[k v]]
             (let [new-path (conj path k)]
               (if (map? v)
                 (pathify v new-path)
                 [new-path v]))) m))
  ([m]
   (pathify m [])))

👀 1
craftybones17:04:16

Importantly, why exactly do you want this?

teodorlu17:04:05

1. curiosity 2. to flexibly transform map keys/paths. First transform into vector of (path value), then work on items with map/filter/reduce, then turn it back into a nested map with assoc-in. This question by @UNRPUL2CT got me hooked: https://clojurians.slack.com/archives/CJ322KHNX/p1648657000940199

teodorlu17:04:33

https://clojurians.slack.com/archives/C053AK3F9/p1649091761535399?thread_ts=1649090531.582539&amp;cid=C053AK3F9 @UK0810AQ2 nice, this is a start:

(defn paths [m]
  (letfn [(paths* [ps ks m]
            (reduce-kv
             (fn [ps k v]
               (if (map? v)
                (paths* ps (conj ks k) v)
                (conj ps (conj ks k))))
             ps
             m))]
    (paths* () [] m)))

(paths {:a 99
        :x {:b 999
            :y {:z 123
                :w 456}}})
;; => ([:x :y :w] [:x :y :z] [:x :b] [:a])
Still need the values.

Ed17:04:42

ok ... here's my attempt:

(defn todo
  ([m]
   (todo m [] []))
  ([m path acc]
   (if (seq m)
     (-> (into acc (comp (remove (comp map? second))
                      (mapcat #(update % 0 (partial conj path)))) m)
         (into (comp (filter (comp map? second))
                     (mapcat (fn [[p m]] (todo m (conj path p) [])))) m))     
     acc)))

1
teodorlu17:04:27

@U01AKQSDJUX care to explain your thinking? I'm not really understanding what's going on in there. Your recursive call seems to carry a map, a "current" path and the accumulated results so far?

(defn todo-ed
  ([m]
   (todo-ed m [] []))
  ([m path acc]

Ed17:04:14

Yeah ... pretty much. I've tried to split up the map into keys that don't need recursive calls and ones that do (i.e. is the val a map?). Then the recursive calls will bottom out at the leaves, with the path being accumulated ... but as soon as you asked me to explain it, I realised you probably don't need to go through the input map so many times:

(defn todo
  ([m]
   (todo m [] []))
  ([m path acc]
   (if (seq m)
     (into acc (mapcat (fn [[p m]]
                         (if (map? m)
                           (todo m (conj path p) [])
                           [(conj path p) m]))) m)     
     acc)))
which is probably easier to read facepalm

👀 1
Ed17:04:45

so I've used the 1 arity to set some defaults (rather than defining an other function, but I'd probably create a todo* or something as a private fn if it was a lib instead)

Ed17:04:29

and then acc accumulates the return value, so we can return it at the end

Ed17:04:43

and path collects the vector that describes where the m value is found in the original input

teodorlu17:04:57

I seem to be getting different results for your second function:

(defn todo-ed2
  ([m]
   (todo-ed2 m [] []))
  ([m path acc]
   (if (seq m)
     (into acc (mapcat (fn [[p m]]
                         (if (map? m)
                           (todo-ed2 m (conj path p) [])
                           [p (conj path m)]))) m)
     acc)))

(todo-ed2 {:a 99
       :x {:b 999
           :y {:z 123
               :w 456}}})
;; => [:a [99] :b [:x 999] :z [:x :y 123] :w [:x :y 456]]

Ed17:04:02

apologies ... silly typo:

(defn todo
  ([m]
   (todo m [] []))
  ([m path acc]
   (if (seq m)
     (into acc (mapcat (fn [[p m]]
                         (if (map? m)
                           (todo m (conj path p) [])
                           [(conj path p) m]))) m)     
     acc)))

oddsor17:04:09

the else-branch in the if statement is conj’ing the value onto the path instead of conj’ing the key onto the path. See my example at the top of the thread

teodorlu17:04:17

@UDB2Q0W13 nice! Somehow I glossed over your reply.

oddsor17:04:31

I do agree with @U0P0TMEFJ that I’d probably have a private or inlined function that handles the looping so that “consumers” wouldn’t be able to add silly values in there 🤷

Ed17:04:15

skimming through the thread that you linked to, you might want to join the keywords together in some kind of hierarchical fashion, but what's the plan when you find a sequential value like a vector rather than a map?

👀 1
teodorlu17:04:43

@U01AKQSDJUX @UDB2Q0W13 Ideas on how to mitigate StackOverflow for deeply nested maps? Do we need a work queue?

(defn deeply-nested-map
  "Create an n-levels nested map to value v"
  [n v]
  (let [keywords (repeatedly n #(rand-nth [:x :y :z :u :v :w]))]
    (reduce (fn [acc newkeyword] {newkeyword acc})
            v
            keywords)))

(deeply-nested-map 4 "value")
;; => {:x {:z {:y {:z "value"}}}}

(do
  (pathify (deeply-nested-map 4 "value"))
  :ok)
;; :ok

(do
  (pathify (deeply-nested-map 10000 "value"))
  :ok)
;; StackOverflow

(do
  (todo-ed2 (deeply-nested-map 4 "value"))
  :ok)

(do
  (todo-ed2 (deeply-nested-map 10000 "value"))
  :ok)
;; StackOverflow

teodorlu17:04:11

@U01AKQSDJUX the idea was to for example filter combinations of paths and values, then turn the result back into a normal map. I'll try to write up some code.

Ed17:04:54

oh totally ... it's consuming the stack to traverse the data structure

teodorlu17:04:24

My head is spinning with recursion now 😅

Ed17:04:10

😉 ... and I need to go ... but sometimes you can use lazyness to get around stack overflows

👍 1
Ed17:04:40

and I'm still not sure what the value of doing this is ... but it's fun 😉

teodorlu17:04:55

Thanks for your contribution!

teodorlu18:04:32

I must apologize - for the whole time, I've been asking for a structure like this:

([:a] 99 [:x :b] 999 [:x :y :z] 123 [:x :y :w] 456)
When in reality, a sequence of pairs would have been way better:
(([:a] 99) ([:x :b] 999) ([:x :y :z] 123) ([:x :y :w] 456))

teodorlu18:04:54

But then we can do stuff like "remove any value that has :y as part of its path:

(->> {:a 99
       :x {:b 999
           :y {:z 123
               :w 456}}}
      normalize
      (partition 2)
      ;; do whatever
      ;; We don't like y's
      (remove (fn [[path _value]]
                (contains? (into #{} path) :y)))
      denormalize)
;; => {:a 99, :x {:b 999}}

oddsor18:04:56

Hmm… Not sure how to deal with stack overflow in an elegant way :thinking_face: But getting a sequence of pairs seems like it would just be a matter of doing (partition 2 (pathify x))

teodorlu18:04:34

Yup, exactly what I'm doing in my last comment.

👀 1
teodorlu18:04:21

Normalize is your function, or Ed's. Denormalize creates maps again with reduce:

(defn normalize
  ([m]
   (pathify m []))
  ([m path]
   (mapcat (fn [[k v]]
             (let [new-path (conj path k)]
               (if (map? v)
                 (pathify v new-path)
                 [new-path v]))) m)))

(defn denormalize [xs]
  (reduce (fn [m [path v]]
            (assoc-in m path v))
          {}
          xs))

🙌 1
fs4219:04:04

Cool! I'll give it a try... thanks for all the leg work to get to this!

🙌 1
Ed22:04:16

if you want path/value pairs you can replace mapcat with map ... maybe something like

(defn normalise
  ([m]
   (normalise m []))
  ([m path]
   (map (fn [[p m]]
          (if (map? m)
            (normalise m (conj path p))
            [(conj path p) m])) m)))
?

Nundrum23:04:59

Would one of you kind souls explain what role Component/Integrant/Mount fills? Is this like the Clojure version of init/systemd? When is a codebase complex enough to warrant using them?

dorab23:04:32

Somewhat. Systemd is for processes that need to be brought up / down in some dependency sequence. Component/Integrant/Mount are to manage global state within your application and to create/teardown that state in dependency order. The README for Component provides a good rationale and use case. As to when to use it, I'd say "from the beginning". Though, if you have only one (or maybe 2) global state to manage, you can probably do it yourself. OTOH, if you're starting out, it might be good to experience the pain of doing it yourself first and then use Component et al. It'll give you a better appreciation of what the libraries do.

seancorfield00:04:47

Things we use Component for at work: setting up a connection pool for database access, starting up an HTTP web server in our app, setting up caches (`clojure.core.cache.wrapped`), etc. This makes it easier to work in the REPL since you can start your app from the REPL and also stop it (and then restart it) as needed.

seancorfield00:04:55

https://github.com/seancorfield/usermanager-example might help you understand Component "in the real world". The README also has a link to a version of the user manager app with Integrant instead of Component.

didibus03:04:01

The main use-case was to enable the "reloaded" workflow. The use case is such: I'm a the REPL, and I want to restart all my REPL state back to how it is on a fresh start with all of my latest changes to the code taken into account. But I don't want to restart my REPL because that is too slow.

didibus03:04:26

So Component was built, by having all the state managed by Component, it can dynamically teardown all the state, unload all namespaces and functions, reload all of it, and restart all your state.

didibus03:04:24

Later, people also realize it might just be a nice way to architect the state in your application, and lets you substitute some of it for mocks or others when running tests.

didibus03:04:55

Finally, people also realized it can be used to configure different state on a per-environment basis. For example, if you have some API client that you initialize to make remote calls too, in staging you can have Component setup the staging state, which will have this client configured to call staging remote APIs. In production you can have Component start the prod client, configured to call the prod endpoints.

💡 1
didibus03:04:26

As to when a code base is complex enough? I'd say only ounce you start to have pain points to which it seems those libraries would remediate.

Nundrum14:04:52

Thanks, everyone. @U0AT6MBUL I built a largeish project (*cloc*s in at 5200 lines) and did the state management myself. That's why I was wondering what I was missing about these libs.