Fork me on GitHub
#beginners
<
2021-10-01
Benjamin09:10:29

(into
 []
 (apply
  concat
  (for [{attachments :attachments} example-messages]
    (seq (map :proxy-url attachments)))))

;; is it wierd to 'apply concat' to get rid of nils?

vanelsas09:10:17

You could use (filter identity ...) as an alternative

Stuart10:10:30

^^ (filter identity) seems more intuitive of its intent to me.

Fredrik10:10:46

Or (remove nil? ...)

Fredrik10:10:01

Or use a :when clause inside the for binding vector

Ben Sless10:10:08

(into [] (keep (comp :proxy-urls :attachments)) messages)) There's an easier way

Ben Sless10:10:15

Whenever you apply concat or flatten to remove nils or empty sequences, regard it as a smell. You can always produce a sequence or transducer which won't have them to begin with

flowthing10:10:28

Or use the transducer arity of into:

(def example-messages
  [{:attachments [{:proxy-url "foo"}]}
   {:attachments [{:proxy-url "bar"}]}
   {:attachments [{:proxy-url nil}]}
   {:attachments []}])

(into []
  (comp
    (mapcat :attachments)
    (keep :proxy-url))
  example-messages)
;;=> ["foo" "bar"]

☝️ 3
👀 3
Ben Sless10:10:40

Missed that attachments are a sequence, this is the correct solution

Benjamin10:10:34

this is awesome thanks

noisesmith15:10:38

in the original (seq (map ...)) is redundant, concat deals with nils just fine

didibus19:10:15

To your question, I would say it isn't weird. Maybe a little confusing to the reader, but a comment about it or like others suggested a more obvious filter or remove would make it readable in my opinion. I think what others are saying relate to performance, where if you can avoid the nils in the first place you'd get a small performance gain, but I would not be concerned with that unless it actually was an issue personally.

didibus19:10:34

Though I would probably just write:

(->> example-messages
  (mapcat :attachments)
  (map :proxy-url)) 
Or if you really want it back as a vector:
(->> example-messages
  (mapcat :attachments)
  (map :proxy-url)
  vec)
Or if you really cared about wanting it as a transducer:
(into []
  (comp
   (mapcat :attachments)
   (map :proxy-url))
  example-messages)

didibus20:10:55

And use keep instead of map if you also want to exclude nil :proxy-url values, but your original code doesn't so I used map to keep the exact same behavior.

quan xing10:10:45

The Clojure is dynamic type. so I can't known what value inside the argument when I read the function. I need read code with context for understand. for example: In java class Student{ String name; Integer age; } foo(Studnet stu){ ...} I know what is stu. In clojure: (defn foo [ stu] ...) I don't noting inside stu. expect read the call foo function position. So. How do I understand this difference in Clojure

dgb2311:10:47

You can tackle this in many ways which include: - consistent naming; See the core library or the book “Elements of Clojure” for good references. - docstrings and other metadata; They are in-built in defns to provide information and documentation. - contracts; You can put pre/post conditions into functions or write specs for data structures and functions alike. Clojure core has done a ton of work there to provide you with good error messages via specs, you can also use/build them to generate tests or to hook up instrumentation. - type annotations; They are a type of metadata that you can attach to arguments. In specific cases they are used to avoid reflection by the compiler. - third party libraries; there are libraries like schema, malli, typed and more that can help you solve this.

flowthing12:10:44

You can also use the REPL. For example:

;; Call clojure.pprint/pprint on every tapped value
(add-tap (bound-fn* clojure.pprint/pprint))

(defn foo
  [student]
  ;; Tap the student argument
  (tap> student)
  ;; Do whatever
  ,,,)

;; Call foo, or do whatever that results in a call to foo
(foo {:last-name "Smith"})
;; student gets printed into stdout
Printing the value is kinda the most lo-fi solution there is. You can also look into using tools like Portal or Reveal.

didibus20:10:29

I would also say, you need to think differently. In Java, you write methods that operate over the class fields. Which mean that you need to know what the fields are when writing the function and you expect the function to work over those fields. In Clojure you should write a function that operates over its own model. The function shouldn't have to care about some external representation. So for example, you might have:

(defn greet-student
  [{:keys [name] :as student}]
  (println "Hello " name))
Now you might ask, what is student, and the answer is that student is any map with a key called :name

didibus20:10:16

There is no real concept of a student as anything more then that. There are no defined student entity, just a function that greets the :name key on a map which we assume might represent a student conceptually.

didibus20:10:31

That means now I can do all kind of things:

(greet-student {:name "Bob" :age 24}
(greet-student {:name "John" :email "})
(greet-student {:name "Emma"})
(greet-student {:name (:student-name student))
All these are valid, so my function is automatically quite generic, it works on many things as long as in the end I can adapt them to a map with a :name key then I can use my function.

didibus20:10:21

Put in another way, you have to ask yourself, why do you care what is inside? And in Clojure, you should not have to care, if you have too, you might be designing things in an OO fashion and should try to re-think. Ideally in Clojure, it is the caller that needs to ask themselves: "What argument does this function want?", it should not be the callee that has to ask itself: "What is the structure of what I am given?" The latter is a code-smell in my opinion.

Stuart11:10:56

If I have a collection of strings (urls), and a function that queries that url that gets some details, is it normal to use map here

(map my-http-query urls)
My thinking with map is that the fn being mapped should be pure, or is this wrong? If its not wrong is their a more idiomatic side-effecting function that collects the results in the same vein as map ? If I use map could its laziness be an issue (maybe I wrap it in doall ? Or just use mapv

dgb2311:10:47

Yes, lazyness could be an issue, (you might not notice it with small sequences). I would wrap it in a doall for sure. It also makes it more explicit that my-http-query is side-effecting.

emccue14:10:37

Personally i'd use mapv - more explicit than a (doall (map ..))

dgb2314:10:22

mapv (or transducers) are explicit about being eager, not so explicit about side-effects

Mario C.15:10:57

@U01EFUL1A8M Could you elaborate on that? I've been using mapv when mapping through side-effects

dgb2315:10:16

@UB0S1GF47 which is fine, I’m nitpicking. If you wrap something into a doall then there the code basically screams “side-effect”. Using mapv is just eager evaluation and specific to vectors. Personally doall feels more right to me. https://clojuredocs.org/clojure.core/doall

Mario C.15:10:53

Ahh I see what you mean now. mapv just so happens to 'run' the side-effect but the intent is implicit. I tend to wrap any side-effect code in functions with an ! at the end. But perhaps a doall might also help to serve as a signal as you say

☝️ 1
didibus20:10:15

I don't think it matters much if you use mapv or doall, personally I find mapv more convenient since its shorter to type. I've never really felt like I needed doall to scream at me side-effect in order to not shoot myself in the foot.

Benjamin14:10:12

Would you put "docker build" stuff into tools.build code?

Andrés Rodríguez14:10:54

What do you call "docker build" stuff? If you mean a few simple lines that call out to docker build, I'd reach for a shell script. No need to launch a JVM and it's made for handling CLIs. If you have a complex set-up making calls to the Docker API to build and/or run a lot of images in a very non-trivial way, I'd reach for Clojure (or any language where tooling is available team-wide).

Benjamin15:10:59

I see that makes sense.

didibus20:10:05

Since I love Clojure, I obviously want to use it for every code I ever need to write, so instead of a shell script in Bash or Python, I personally use Babashka.

didibus20:10:15

I also maybe disagree with @U02FU034U15, unless I need it to start really fast, I see no problem using Clojure JVM when you're already setup to be able to run Clojure. So I think also just using tools.build is fine if it lets you do what you want.

Benjamin15:10:07

WARNING: update-keys already refers to: #'clojure.core/update-keys in namespace: clojure.tools.analyzer.utils, being replaced by: #'clojure.tools.analyzer.utils/update-keys
I know it's a bit ghetto to ask like this without a minimal reproduce but I have those warnings since I added org.clojure/clojure {:mvn/version "1.11.0-alpha2"} dep. Is there some obvious reason for this?

didibus20:10:18

Ya, a lot of existing code used to define their own update-keys, but now its a core function. You can mostly ignore the warning, and hopefully libraries will eventually update themselves as well.

vncz15:10:25

I’ve also seen this @benjamin.schwerdtner

vncz15:10:56

I guess that namespace was specifying that utility function on its own and it’s now conflicting with the builtin one in clojure.core

noisesmith15:10:58

@benjamin.schwerdtner this is a periodic thing when clojure adds a new core function with an obvious or common name, for example update caused the same warnings in many libs when it was introduced

sova-soars-the-sora16:10:35

I want to run through a vector of maps and if a map in question has a particular key-value I want to add 2 new key-vals to the map in the vector-of-mapz.

sova-soars-the-sora16:10:05

For example

[ {:id 1 :hax 5} 
  {:id 2 :hax 3} 
  {:id 7 :hax 9} ]
If there's a match on let's say :hax 9 I'd like to add 2 new keys to that map
[ {:id 1 :hax 5} 
  {:id 2 :hax 3} 
  {:id 7 :hax 9
   :top "sweet"
   :Bottom "nice"} ]

sova-soars-the-sora16:10:18

I was thinking this could be a job for reduce but I'm not sure how I'd make it work.

pyry17:10:04

I think just (map update-if-matching-kv vector-of-mapz).

pyry17:10:15

map instead of reduce feels natural, as what you want is a simple element-wise operation; reduce would come in handy if you were aggregating elements together

sova-soars-the-sora17:10:13

So then update-if-matching-kv could take the map in and output a new map with assoc'd new keys... if I understand you right

sova-soars-the-sora17:10:27

😄 🙏:skin-tone-4:

sova-soars-the-sora17:10:03

What if I wanted to pass in specific new keys and vals ? (map update-if-matching-kv vector-of-mapz {new-keys, new-vals}) ?

sova-soars-the-sora17:10:37

{new-keys, new-vals} would probably have to be an arg in the update-if-matching-kv now that I look at it

sova-soars-the-sora17:10:07

I guess map with 2 input maps confuses me right now

sova-soars-the-sora17:10:49

Oh thanks, I figured it out using partial (map (partial assoc-if-matching-kv {:le-key "le-val"}) vector-of-mapz)

sova-soars-the-sora17:10:10

and assoc-if-matching-kv takes 2 maps 😎

sova-soars-the-sora22:10:02

o.O neat. I don't really get it though

didibus20:10:34

Like this?

(for [m [{:id 1 :hax 5}
         {:id 2 :hax 3}
         {:id 7 :hax 9}]]
  (if (= (:hax m) 9)
    (assoc m :top "sweet" :bottom "nice")
    m))

didibus21:10:27

> What if I wanted to pass in specific new keys and vals ? > (map update-if-matching-kv vector-of-mapz {new-keys, new-vals}) Closures are your friend:

(defn map-assoc-if-kv
  [coll k v & new-kvs]
  (for [m coll]
    (if (= (k m) v)
      (apply assoc m new-kvs)
      m)))

(map-assoc-if-kv
 [{:id 1 :hax 5}
  {:id 2 :hax 3}
  {:id 7 :hax 9}]
 :hax 9
 :top "sweet"
 :bottom "nice"
 :side "yuk")
;;=> ({:id 1, :hax 5} {:id 2, :hax 3} {:id 7, :hax 9, :top "sweet", :bottom "nice", :side "yuk"})

didibus21:10:50

Which you can rewrite using map like so:

(defn map-assoc-if-kv
  [coll k v & new-kvs]
  (map
   #(if (= (k %) v)
      (apply assoc % new-kvs)
      %)
   coll))
So you see, you don't need to pass the new-kvs and the k and v to compare against to the function used for mapping, instead you can have the function close over them using closures.

didibus21:10:22

For composability, it might be better to separate the mapping with the assoc logic though like:

(defn assoc-if-kv
  [m k v & new-kvs]
  (if (= (k m) v)
    (apply assoc m new-kvs)
    m))

(map
  #(assoc-if-kv % :hax 9 :top "sweet" :bottom "nice")
  [{:id 1 :hax 5} 
   {:id 2 :hax 3} 
   {:id 7 :hax 9}])

didibus21:10:06

So you can now reuse it when you have a single map or a list of maps as well

sova-soars-the-sora23:10:26

Sweet! 😄 thank you @U0K064KQV and thanks for going from for to map that's really nice to see the side-by-side