Fork me on GitHub
#beginners
<
2019-06-15
>
johnj00:06:03

is there a function to return a map entry as a map if key is found?

andy.fingerhut00:06:52

Do you mean, if a key k is in a map m, you want the return value to be a map m2 that contains only the key k and its associated value?

andy.fingerhut00:06:41

select-keys can do that, and more.

andy.fingerhut00:06:15

user=> (select-keys {:a 1 :b 2 :c 3} [:b])
{:b 2}
user=> (select-keys {:a 1 :b 2 :c 3} [:b :a])
{:b 2, :a 1}

andy.fingerhut00:06:50

It returns an empty map (0 keys) if none of the given keys are found in the map.

johnj00:06:03

wonderful! thanks

noisesmith00:06:18

if you use not-empty that empty map becomes nil (useful for conditionals)

johnj00:06:39

was building something up with merge, forgot about select-keys

noisesmith00:06:55

(ins)user=> (not-empty (select-keys {:a 0} [:b]))
nil
(ins)user=> (not-empty (select-keys {:a 0 :b 1} [:b]))
{:b 1}

johnj00:06:15

handy, was using seq heh

johnj00:06:32

loosing the map

dpsutton00:06:18

Also find returns the map entry which may be good enough depending on your needs

johnj00:06:37

yeah was using find but I knew clojure had something better built-in for this case, I noticed this is a very common thing to do (the select-keys one)

andy.fingerhut00:06:35

Not sure if you are familiar with the Clojure cheat sheet, but it tries to organize the core Clojure functions by what they do, and/or what kind of data they operate on: https://jafingerhut.github.io Also the version I linked shows doc strings when you hover the cursor over a function name.

andy.fingerhut00:06:02

If you choose one of the ClojureDocs variants, clicking on a function name takes you to the http://ClojureDocs.org page on the function, with user-supplied examples or comments/gotchas.

johnj00:06:34

yes, knew about this one https://clojure.org/api/cheatsheet, very helpful but stop using it thinking I'm going to missed some more obscured but useful stuff, is there a list somewhere of what its not included?

johnj00:06:10

but really, should have that open all the time

andy.fingerhut00:06:27

There is such a list, but, well, it isn't categorized and organized 🙂

johnj00:06:55

online? I'll take it

andy.fingerhut00:06:06

This list is about a year old, so might be missing some things from Clojure 1.10 -- don't remember when that came out relative to May 2018 when this list was generated: https://github.com/jafingerhut/clojure-cheatsheets/blob/master/src/clj-jvm/TODO.txt#L207-L424

andy.fingerhut00:06:56

I guess I could publish that list a little more prominently for folks that feel like they might be missing out, but really I expect that most of what you are missing out on are the common useful Clojure and Java libraries, which that cheatsheet isn't even trying to cover.

johnj00:06:08

quickly skimming looks like read and read-string are in both (the cheatsheet and exclude list), I'll see if can try cleaning it up a bit

johnj00:06:45

by 'common useful clojure libraries' you mean third party?

andy.fingerhut00:06:08

The versions of read and read-string in the cheatsheet are only the clojure.edn versions, by design. I wanted to make it harder to find and use the more dangerous ones in clojure.core

andy.fingerhut00:06:59

Or maybe I'm wrong on that. It shows the version in the 3rd party lib clojure.tools.reader, but not the ones in clojure.edn? That seems a bit odd to me.

johnj00:06:49

right, click on it to find out, took me to tools.reader

andy.fingerhut00:06:56

Yeah, I should add the ones in clojure.edn namespace, too.

johnj00:06:18

anyway, looking more at it, that cheatsheet is amazing, thanks

andy.fingerhut00:06:18

No problem. If you find useful stuff missing, or mis-organized, feel free to create an issue on the Github repo, or drop me a line somehow

👍 4
andy.fingerhut00:06:07

It does mention a few third party libraries that are not part of Clojure itself, but is nowhere near extensive in that regard. The Clojure Cookbook is something I've been going through recently, and it is fairly good for that kind of thing for a lot of tasks.

johnj01:06:10

`select-keys' is just showed onced inside some example (not related to maps) in 'clojure programming' and zero in 'programming clojure'

andy.fingerhut01:06:34

Not trying to worry you here, but I just added clojure.edn/read and /read-string to my version of the cheatsheet (the http://clojure.org version is updated less often -- should do that soon-ish). I also updated the list of symbols not in the cheatsheet, and it is now 400+ long.

johnj01:06:40

cool, what's the difference between clojuredocs and grimoire?

johnj01:06:30

ah different doc sites

andy.fingerhut01:06:30

yes. Grimoire was started at a time when clojuredocs had not updated to the latest Clojure version for a while, and now ClojureDocs is up to date again and Grimoire may be no longer maintained (unless someone volunteers)

johnjelinek01:06:26

(clojure.repl/doc secretsmanager/describe-secret)
-------------------------
amazonica.aws.secretsmanager/describe-secret
([describe-secret-request])
wat .. what am I supposed to make of this? what is a describe-secret-request?

johnjelinek01:06:41

guessing my way to success ¯\(ツ)

andy.fingerhut01:06:57

Well, it's secret .... 🙂

andy.fingerhut01:06:54

More seriously, perhaps the authors of amazonica were expecting that people would look up the docs for the corresponding AWS APIs, presumably documented elsewhere?

andy.fingerhut01:06:22

I do not know how Amazonica relates (if at all) to the aws-api library published by Cognitect, but you can take a look there to see if its docs are any nicer: https://github.com/cognitect-labs/aws-api/

johnjelinek01:06:38

@andy.fingerhut: I started with aws-api, but I'm having to backtrack because it doesn't seem to play nice with jdk8, which is a constraint for me atm

johnjelinek02:06:56

new question: I made a spec (s/def ::stage string?) ... but I'd like to more specifically constrain the value to either be one of PENDING, CURRENT, or PREVIOUS ... is there a special spec-y way of defining this?

andy.fingerhut02:06:35

I believe one way this is done is to spec it as a set of the values you wish to allow, e.g. #{"PENDING" "CURRENT" "PREVIOUS}

andy.fingerhut02:06:58

Sets in Clojure can be "called" as if they were functions, and return the first argument if it is in the set, else nil.

😮 4
johnjelinek02:06:12

nice, that works, thanks!

johnjelinek03:06:26

what's the idiomatic way to store and share s/defs?

johnjelinek03:06:58

I imagine I can't ::refer to them across namespaces

seancorfield03:06:11

specs are referenced just by name.

seancorfield03:06:39

As long as the namespace in which they are defined has been required, you can access them by name.

johnjelinek03:06:25

as long as they're not overloaded in the current namespace, right?

seancorfield03:06:10

specs use qualified keywords -- they're unique

seancorfield03:06:22

:foo/bar regardless of namespace, for example.

seancorfield03:06:36

If you have (s/def ::bar ...) then it'll be :whatever.this.namespace/bar so it'll be unique to the namespace it is defined in.

johnjelinek04:06:19

so, if you have a (s/def ::secret-id string?) and that's a common primitive you want to use in other (s/def...)s, would you put that in a common namespace and have everything use the qualified name? :wat/secret-id?

seancorfield04:06:44

Specs don't have to correspond to namespaces. They're "just" qualified keywords.

seancorfield04:06:05

It's just a convenience to use existing namespaces as those qualifiers sometimes.

johnjelinek03:06:13

(defn -handleRequest [this is os context]
  (let [w (io/writer os)]
    (-> (util/json-is->map (io/reader is))
        (pprint)
        [{::secret-id :secret-id
          ::client-request-token :client-request-token
          ::step :step}]
        (route-event)
        (json/write w))
    (.flush w)))
is this how I would destructure and map properties off a map into qualified keywords?

johnjelinek04:06:29

hmm ... I suppose not

johnjelinek04:06:39

yea, it doesn't like this:

(let [{::secret-id :secret-id} event]
  ::secret-id)

johnjelinek04:06:56

it'll take this though:

(let [{secret-id :secret-id} event]
  secret-id)

johnjelinek04:06:07

hmm ... this doesn't work though:

(defn -handleRequest [this is os context]
  (let [w (io/writer os)]
    (-> (util/json-is->map (io/reader is))
        (pprint)
        (fn [e]
          (let [{id :secret-id
                 token :client-request-token
                 step :step} e]
            {:spec/secret-id id
             ::client-request-token token
             ::step step}))
        [{::secret-id :secret-id
          ::client-request-token :client-request-token
          ::step :step}]
        (route-event)
        (json/write w))
    (.flush w)))

seancorfield04:06:58

(let [{:prefix/keys [foo bar] ...] ...)

seancorfield04:06:15

That will match :prefix/foo and :prefix/bar.

seancorfield04:06:51

You can't (-> into a (fn [e] form. -> is a pure syntax transform

seancorfield04:06:29

You really need to get into the habit of experimenting with small forms in the REPL and macroexpanding things.

seancorfield04:06:03

The above code makes no sense at all with -> and you'd see that if you developed a REPL-based workflow.

seancorfield04:06:32

I really do recommend Eric Normand's online course REPL-Driven Development. Definitely worth paying for.

seancorfield04:06:31

Part of the problem with code like that is if you -> a value into a pprint call, you get nil out. This is why you need to work with expressions in the REPL more.

johnjelinek04:06:31

can you linkme to the course? I'll buy it!

seancorfield04:06:53

Nearly 30 lessons and about 8-9 hours of video. Totally worth it.

johnjelinek05:06:03

(clojure.repl/doc kashoo/refresh-bearer-token)

dal.kashoo/refresh-bearer-token

([credentials])

  refresh kashoo bearer token

Spec

  args: (keys :req [:dal.kashoo/client-id :dal.kashoo/client-secret :dal.kashoo/refresh-token])

  ret: map?
but then when I call it, it's failing {:pre [(s/valid? ::credentials credentials)]}
(let [api-key ...
        credentials {:kashoo/client-id (:client_id api-key)
                     :kashoo/client-secret (:client_secret api-key)
                     :kashoo/refresh-token (:refresh_token bearer-token)}]
    (kashoo/refresh-bearer-token credentials))

johnjelinek05:06:49

I've already required [dal.kashoo :as kashoo] ☝️

johnjelinek05:06:13

so, why is this failing to validate?

Assert failed: (s/valid? :dal.kashoo/credentials credentials)

seancorfield05:06:26

::kashoo instead of :kashoo.

seancorfield05:06:37

You want :: so it resolves the alias.

johnjelinek05:06:04

I thought I could only do :: if I'm in the namespace where it's qualified

seancorfield05:06:35

::f/g will resolve f as an alias.

👍 4
seancorfield05:06:47

::g will resolve to the current namespace.

seancorfield05:06:29

Trying this stuff out in the REPL would show you how it worked...

👍 4
johnjelinek05:06:41

(I'm actually getting all the pastes above from my REPL where I'm trying it)

seancorfield05:06:18

But you're not trying it in small enough pieces. That's the habit you need to develop.

seancorfield05:06:50

To be productive in Clojure you need to start thinking at the level of the form, not the function, not the file.

seancorfield05:06:09

And you need to get used to evaluating small pieces.

seancorfield05:06:59

In the above case, the spec says you need :dal.kashoo/client-id which :kashoo/client-id is not -- and you can verify that at the REPL.

seancorfield05:06:35

But you can also verify that with your kashoo alias, ::kashoo/client-id would resolve to :dal.kashoo/client-id

seancorfield05:06:46

That's why I recommend Eric's course.

👍 4
seancorfield05:06:09

And I'd also recommend Stu Halloway's Debugging With The Scientific Method and Running With Scissors.

👍 4
4
✂️ 4
johnjelinek05:06:53

is that a book?

seancorfield05:06:26

Another online presentation.

seancorfield05:06:44

It's a very different workflow from other languages. But it's very tangible. Everything is available in the REPL. Docs, source, tests, exploration.

👍 4
johnjelinek05:06:45

is the size of this function hairy?

(defn- create-secret []
  (let [bearer-token (-> (secretsmanager/read-secret {:spec/secret-id dev-kashoo-bearer-token})
                         (get :secret-string)
                         (util/json->map))
        api-key (-> (secretsmanager/read-secret {:spec/secret-id (:masterarn bearer-token)})
                    (get :secret-string)
                    (util/json->map))
        credentials {::kashoo/client-id (:client_id api-key)
                     ::kashoo/client-secret (:client_secret api-key)
                     ::kashoo/refresh-token (:refresh_token bearer-token)}
        access-token (kashoo/refresh-bearer-token credentials)
        bearer-token (merge bearer-token {:access_token (:access_token access-token)
                                          :refresh_token (:refresh_token access-token)})]
    bearer-token))

seancorfield05:06:08

Way too big. And way too imperative.

seancorfield05:06:55

That should be four or five small functions -- that represent expressions you can easily test in the REPL.

johnjelinek05:06:36

ok, so, basically make each of those let assignments map to a single function

johnjelinek05:06:05

but that wouldn't fix the imperative piece

seancorfield05:06:31

Sure, there's going to be some imperative piece at the top of the call chain.

seancorfield05:06:54

But you want each piece to be easy to eval/test in the REPL.

seancorfield05:06:16

That way you catch problems quickly and easily -- and early.

seancorfield05:06:08

It's also probably not a good idea to bind bearer-token twice in that let.

seancorfield05:06:34

(assuming you're familiar with a strictly TDD workflow 🙂 )

johnjelinek05:06:17

yea, I'm familiar -- we have a watcher that runs our golang test suite on every file save

seancorfield05:06:34

That's a horrible workflow 😞

johnjelinek05:06:49

takes 30 seconds every time we make a change to see red or green

seancorfield05:06:53

But it's what people get used to in other languages as the "best they can do"

seancorfield05:06:01

30 seconds is a LONG time.

seancorfield05:06:36

Feedback should be instantaneous.

seancorfield05:06:03

Write an expression, eval, see the result. Instantly. Over and over again.

seancorfield05:06:54

Those first two strings are screaming out for a function.

(defn read-secret [data]
  (-> (secretsmanager/read-secret {:spec/secret-id data})
      (:secret-string)
      (util/json->map)))

seancorfield05:06:36

Naming is hard 🙂

seancorfield05:06:52

But, yeah, small functions. Easily testable in the REPL. Do just one thing. Compose the results.

johnjelinek05:06:33

I did this one:

(defn read-secret-string
  "read-secret-string only returns the secret-string of a secret"
  [secret]
  {:pre [(s/valid? ::secret secret)]
   :post [(s/valid? map? %)]}
  (-> (read-secret secret)
      (get :secret-string)
      (util/json->map)))

johnjelinek05:06:04

and refactor:

(defn- create-secret []
  (let [bearer-token (secretsmanager/read-secret-string {::spec/secret-id dev-kashoo-bearer-token})
        api-key (secretsmanager/read-secret-string {::spec/secret-id (:masterarn bearer-token)})
        credentials {::kashoo/client-id (:client_id api-key)
                     ::kashoo/client-secret (:client_secret api-key)
                     ::kashoo/refresh-token (:refresh_token bearer-token)}
        access-token (kashoo/refresh-bearer-token credentials)
        bearer-token (merge bearer-token {:access_token (:access_token access-token)
                                          :refresh_token (:refresh_token access-token)})]
    bearer-token))

johnjelinek05:06:41

that's cool ... input-level destructuring

johnjelinek05:06:46

I think merge is not the right thing to use here:

(merge 
 (map-kashoo-credentials {:client_id "a" :client_secret "b"})
 (map-kashoo-credentials {:refresh_token "a"}))
; =>
#:dal.kashoo{:client-id nil, :client-secret nil, :refresh-token "a"}
function:
(defn- map-kashoo-credentials
  [{:keys [client_id client_secret refresh_token]}]
  {::kashoo/client-id client_id
   ::kashoo/client-secret client_secret
   ::kashoo/refresh-token refresh_token})

seancorfield06:06:48

Your problem is you are creating maps with nil values and overwriting non-`nil` values.

seancorfield06:06:46

instead do

(merge
  #:dal.kashoo{:client_id "a" :client_secret "b"}
  #:dal.kashoo{:refresh_token "a"})

seancorfield06:06:11

Or just

(merge
  #::kashoo{:client_id "a" :client_secret "b"}
  #::kashoo{:refresh_token "a"})

seancorfield06:06:40

(again, try this stuff in REPL -- which is what I just did)

seancorfield06:06:49

user=> (alias 'kashoo (create-ns 'dal.kashoo))
nil
user=> (merge #::kashoo{:client_id "a" :client_secret "b"} #::kashoo{:refresh_token "a"})
#:dal.kashoo{:client_id "a", :client_secret "b", :refresh_token "a"}
user=> 

johnjelinek06:06:35

yea, but the problem I have is that each of those keywords come from different maps, so I'm trying to bring them together by taking a subset of each one and then merge

johnjelinek06:06:55

i.e:

(let [credentials {::kashoo/client-id (:client_id api-key)
                     ::kashoo/client-secret (:client_secret api-key)
                     ::kashoo/refresh-token (:refresh_token bearer-token)}])

seancorfield06:06:03

select-keys gives you a subset of a map's keys.

seancorfield06:06:36

Break things down. Merge the keys you want from various maps. Then qualify them if you need to.

👍 4
johnjelinek06:06:34

aight, I think I'm done for the night -- it's 1am here

seancorfield06:06:40

As @deleted-user suggested, it's easy to walk over an entire map and qualify all the keys -- once you have the merged subset of keys you want.

dumrat11:06:28

Hi guys, cider jack in today gives this error:

[nREPL] Starting server via /usr/local/bin/lein update-in :dependencies conj \[nrepl\ \"0.4.5\"\] -- update-in :plugins conj \[cider/cider-nrepl\ \"0.19.0-SNAPSHOT\"\] -- repl :headless :host localhost...
error in process sentinel: nrepl-server-sentinel: Could not start nREPL server: Exception in thread "main" java.lang.RuntimeException: Unable to resolve symbol: create in this context, compiling:(/private/var/folders/g_/ldc4hkys4r73yffyf77pcn1c0000gn/T/form-init7254150147707415858.clj:4223:33)
Any idea why this happens? Used to be fine till yesterday so I'm guessing some update somewhere?

pinkfrog11:06:45

i wonder what’s the use case difference of map and mapv.

john15:06:37

map is lazy and returns a listy thing (a seq). mapv is eager (consumes it from beginning to end right now) and returns the results in a vector

john15:06:46

Sometimes you'll see mapv used not because people actually want the result to be a vector but because they just want an ordered result that computes eagerly

john15:06:06

But you could also do (doall (map ...

didibus17:06:09

Since you asked for the use case. It's mostly when you need side effect and return value

didibus17:06:53

In some cases, mapv can also be faster. If you are going to use the full result set, mapv creates less intermediate result, and should end up being a little faster. The difference will really only show on massive collections.

dmaiocchi13:06:58

Hi! i was searching for a online/link documentation about the ^ symbol used in a def

bronsa13:06:02

it's a reader macro for metadata

orestis13:06:56

Reagent React interop sugar

👍 4
dmaiocchi13:06:44

@bronsa do you know any real use case of metadata ? maybe an example in some projects. tia!

didibus17:06:02

It is used to give hints and directives to the Clojure compiler. You can type hint with it for example. You can specify that some Var should be dynamic or constant. That they shouldn't be re-defed on reload.

didibus17:06:55

It is also used for meta info. For example, the doc-string is stored as metadata. That's why it is available at runtime with the doc function

Iwo Herka14:06:26

Is there any more elegant way to do (filter #(> (count %) 1) coll)?

dpsutton14:06:44

you could have a def'd version somewhere. (defn more-than-one [c] (> (bounded-count 2 c) 1)) and then (filter more-than-one coll) seems pretty elegant to me. I don't see anything wrong with your current implementation, provided you don't have any huge (or infinite) lazy seqs going through it.

👍 4
upgradingdave16:06:00

is there a way to print a dependency tree (similar to lein deps :tree) using deps.edn?

bronsa16:06:59

clj -Stree

💯 4
upgradingdave16:06:14

Geesh, there it is right in the docs and I totally missed it, thanks 😊

Adrian Smith17:06:40

is there a way to place a tagged literal into a data structure dynamically?

dpsutton17:06:56

Can you give an example?

Adrian Smith17:06:14

so in Honeysql you can define a query like :

{:select [#sql/inline 5]}
where
#sql/inline
is a predefined tagged literal that has meaning for that library but I don't know how to dynamically insert one

Adrian Smith17:06:15

Maybe "how to add a tagged literal to a data structure at run time?" is a better way of asking the question?

dpsutton17:06:36

(assoc {} :it-just-works #sql/inline 5)

dpsutton17:06:11

can you ignore the tagged literal and show me how you intend to update a datastructure at runtime and then we can figure out how to integrate that with the tagged literal?

❤️ 4
Adrian Smith17:06:28

The data structure is defined via incoming json, I'm walking over the structure and making several conversions, one conversion I want to make is string of "#sql/inline" to tagged literal of #sql/inline

dpsutton17:06:47

(let [f (fn [x] #sql/inline x)]
  (update {:x 3} :x f))
{:x {:value x}}

dpsutton17:06:03

(get *data-readers* 'sql/inline)

dpsutton17:06:42

(update {:x 3} :x (get *data-readers* 'sql/inline identity))

Adrian Smith18:06:37

I think there's more going on in the reader than I assumed, consider the syntax

[#sql/inline 5]
that ends up being (f 5) just because they're next to each other (I think?), seems like I'll have to resolve to the function like you showed and parse at a higher level then per item?

Adrian Smith18:06:23

I've seen syntax like this too,

[#sql/call [:+ 1 2] "not part of the call"]

john18:06:30

At runtime, couldn't you just run the function that backs the tag reader?

dpsutton18:06:10

Yeah that’s the lookup using the data readers Need to go through the readers map so if the lib changes it in the future it still works

Adrian Smith18:06:58

yep the only thing I didn't realize is a tagged literal is like a pair, a function and its argument consider

#inst "2019-06-06" 
but
#inst
on its own doesn't work, so upstream I need to reconsider how I pass this information maybe something like '{":select" : ["#sql/call [\":+\", \"1\", \"1\"]"]}', then when I walk over the call I have the arguments in the same place

Adrian Smith18:06:58

otherwise my walk needs to be context aware, which I think is possible too but not keen on lots of scanning

john18:06:10

So #sql/whatever is going take the string the right and convert it. You should just be able to call read-string if a fn is registered for it. But otherwise you could just run the function that backs whatever on the string and you'll get the desired thing returned

❤️ 4
dpsutton17:06:14

if you need more than that, tagged literals just invoke a function. Its defined in data_readers.clj in honeysql/resources. And it just calls the function honeysql.types/read-sql-inline which just wraps the value in a record

Adrian Smith17:06:47

Ah I see, I could translate directly to the function call

dpsutton17:06:55

let me look to see if there's a way to get that. presumably its in a map somewhere but you don't want to hardcode it to a particular function in case it changes in the future

johnj19:06:23

What's wrong about using core.clj ? I remember someone here mentioning it but forgot

dpsutton19:06:49

nothing in my opinion.

johnj20:06:37

yeah, don't see nothing that can cause problems

Iwo Herka19:06:53

Is there a function which given a seq will return its constructor function?

() => list
[] => vec
#{} => set
{} => hash-map

andy.fingerhut19:06:54

There is nothing wrong with using the file name core.clj in your project, but there is, I think, a distinct disadvantage: In stack traces, only the last component of source file names appears, not the full path. It is very common for functions in the clojure.core namespace to have core.clj as their file name. If you use core.clj, then your functions will show in stack traces with that file name, too.

johnj20:06:56

aha, sounds important for when debugging an issue but no idea how much of a trouble it would be since I haven't ran into that situation.

andy.fingerhut20:06:04

@hi135 I am not aware of such a function. There is empty, which given any collection, will return an empty collection of that same kind. That may not be useful for your purposes, though.

dpsutton20:06:17

what are you trying to accomplish?

bronsa20:06:06

user=> (def make-from (fn [coll] (fn [els] (into (empty coll) els))))
#'user/make-from
user=> ((make-from [1]) '(1 2))
[1 2]

Iwo Herka20:06:11

@dpsutton I'm playing around reimplementing map:

(defn map*
  [f coll]
  (reduce* (fn [acc x] (conj acc (f x))) [] coll))
The problem is that I always return vector, which means that given a list or a set, I change the type. Passing (empty coll) to reduce works for sets and vectors, but list is reversed (since conj).

dpsutton20:06:54

what does map return?

dpsutton20:06:38

and what i'm getting at is that map doesn't preserve the input type. (set? (map identity #{:a :b}))

Iwo Herka20:06:56

I'm aware that it's LazySeq.

dpsutton20:06:58

(vector? (map identity [:a :b]))

Iwo Herka20:06:19

I'm just playing trying to write it so it does preserve the type.

dpsutton20:06:37

ah ok. One place that is collection aware is in walk

Iwo Herka20:06:49

As an exercise. 🙂

dpsutton20:06:49

you could think on it more and then look there to see how clojure.walk accomplishes it

Iwo Herka20:06:23

Cool, will do. Thanks.