Fork me on GitHub
#beginners
<
2021-05-06
>
Jeff Evans03:05:43

Is it considered bad form to nest -> inside cond-> like this? Suppose I want to both assoc some keys and dissoc some keys if the map contains :a (and perhaps even do more things) Is there a better way of expressing this?

(let [my-map {:a 1 :b 2 :c 3}]
  (cond-> my-map
    (:a my-map) (->
                 (assoc :x 8 :y 9 :z 10)
                 (dissoc :b))))

lsenjov04:05:15

I do it often. The other option is to repeat the same condition which is harder to read imo.

👍 6
Ben Sless06:05:31

Aren't threading macros designed to be composed this way? Or was it just a happy accident?

👍 2
noisesmith16:05:15

@UK0810AQ2 the threading macros are designed to nest inside -> , in this case I think if is more readable / idiomatic

(let [my-map {:a 1 :b 2 :c 3}]
  (if-not (:a my-map)
     my-map
     (->> my-map
          (assoc :x 8 :y 9 :z 10)
          (dissoc :b))))

noisesmith16:05:32

but the first example is OK

Jeff Evans17:05:08

yeah I was thinking more where there were potentially multiple “rules” that should apply. the nice thing about cond-> is that all matching branches will happen, unlike cond

Jeff Evans17:05:47

though I suppose at a certain point it could be argued that needs to be broken up into different fns

noisesmith17:05:00

oh yeah, definitely use cond-> if there are more than one conditional

zackteo07:05:32

Hello, after doing partitioning how do you merge parts of the resulting sequence based on a condition like from [["content"] ["*"] ["content"] ["*"] ["content"]] to [["content"] ["*" "content" "*"] ["content"]]

didibus07:05:52

Maybe with split-with ?

zackteo07:05:29

I just tried using someone's implementation of split-by to do the partitioning of sorts. But that ended up with giving me [["content"] ["*" "content"] ["*" "content"]]

raspasov08:05:50

I don’t understand what’s the condition for merging.

raspasov08:05:32

Is it every time you encounter "*" ?

zackteo08:05:11

Yeah. Something like that. I just made it general

yuhan08:05:57

is * something like a delimiter?

zackteo08:05:14

Am actually using some-fn to join a str/starts-with and str/end-with for the purposes of partition out multi-line comments of /* and */

raspasov08:05:37

Ok, let me think for a second what’s idiomatic here… You often come up with cases that I don’t encounter in day-to-day stuff :) Are you working with data input that you don’t control? If yes, then that’s ok. But if you have enough control over the data input, I always try to think how to avoid having to do “magic tricks” like that.

raspasov08:05:15

The best solution to a problem is not having the problem in the first place! 😃

zackteo08:05:26

hmmmm am actually working with a .cpp file like

zackteo08:05:35

#include <stdio.h>

/*
   This is a multi-line comment
   The blanks in multi-line comment is counted as Blanks

*/
int main() {
  /// This is a comment line
  printf("Hello, world! \n "); // Code+Comment line together counted as Code.
  return 0;
}

raspasov08:05:44

Oh, man. Ok 🙂

zackteo08:05:58

Am trying to find the number of lines which are commented

raspasov08:05:00

I guess that very much falls into “I don’t control the input” category

zackteo08:05:12

Right 😅

zackteo08:05:29

(partition-by (some-fn #(str/starts-with? % "/*")
                             #(str/ends-with? % "*/"))
                    (str/split (slurp "resources/test.cpp") #"\n"))

zackteo08:05:30

To get

(("#include <stdio.h>" "")
 ("/*")
 ("   This is a multi-line comment"
  "   The blanks in multi-line comment is counted as Blanks"
  "")
 ("*/")
 ("int main() {"
  "  /// This is a comment line"
  "  printf(\"Hello, world! \\n \"); // Code+Comment line together counted as Code."
  "  return 0;"
  "}"))

zackteo08:05:07

I thought this is a good step towards identifying the number of comments at least in the sense of handling the multi-line case

raspasov08:05:49

Hm …. Perhaps https://clojuredocs.org/clojure.core/line-seq … And look for /* and */ in each line?

zackteo08:05:22

And identified that the next step would be to join the (/) ( ...) ( /) in the same partition

raspasov08:05:02

I see, you’re already basically doing what line-seq does…

yuhan08:05:48

Sounds like a language parsing problem - might be good to use a library like Instaparse for it

yuhan08:05:17

Or if you want to roll your own line-based parser, maybe implement a state machine that represents inside/outside comment and use that to reduce over the sequence of lines

didibus08:05:13

If it's a string I think regex can do it.

raspasov08:05:32

Yes… Library sounds a good idea, if a good one exists for C++ code…

yuhan08:05:48

c comments are particularly weird because they can be nested

zackteo08:05:56

@U010Z9AC90Q I would think I already reduced the problem to converting [["content"] ["/*"] ["content"] ["*/"] ["content"]] to `[["content"] ["/" "content" "/"] ["content"]]`

zackteo08:05:48

Like ignoring nesting of sorts for now

zackteo08:05:43

Can I iterate through the sequence to do something like if / join with the element after it . if / join with the element before it

zackteo08:05:52

Am just thinking if I can use something like map/reduce. Or do I need to use stuff like loop

yuhan08:05:54

probably you need loop or reduce with a simple state machine in the accumulator, that says "I'm inside/outside a comment" and decides what to do with the next element based on that

yuhan08:05:12

There's a library called "seqexp" for this which is like regexes for sequences, basically more high level than writing your own finite state automaton

raspasov08:05:43

(let [source (slurp "/Users/raspasov/cpp")
      lines  (line-seq
              (java.io.BufferedReader.
               (java.io.StringReader. source)))]
 (sequence
  (comp
   ;find start of comment
   (drop-while
    (fn [line]
     (not (clojure.string/includes? line "/*"))))
   ;find end of comment
   (take-while
    (fn [line]
     (not (clojure.string/includes? line "*/")))))
  lines))

raspasov08:05:55

=> ("/*" " This is a multi-line comment" " The blanks in multi-line comment is counted as Blanks")

raspasov08:05:41

This will actually only find the first comment block in the file…

zackteo08:05:00

yeap 😅 yeap I realised hahaha

zackteo08:05:50

I'm still someone convinced there should be a relatively simple way to go from [["content"] ["*"] ["content"] ["*"] ["content"]] to [["content"] ["*" "content" "*"] ["content"]]

zackteo08:05:49

feels like i would've done some form of this in some 4clojure problem at some point 😅

yuhan08:05:47

(let [input                   ["A" "B" "/*" "C" "D" "*/" "E"]
      [before [start & then]] (split-with #(not= "/*" %) input)
      [during [end & after]]  (split-with #(not= "*/" %) then)]
  [before [start during end] after])
;; => [("A" "B") ["/*" ("C" "D") "*/"] ("E")]

zackteo08:05:10

On a side note i was quite amused one someone's implementation of split-by on the split-with clojuredocs Like so.

(-> (split-by (some-fn #(str/starts-with? % "/*")
                       #(str/ends-with? % "*/"))
              (str/split (slurp "resources/test.cpp") #"\n"))
    )
Made the problem become
[["content"] ["/*" "content"] ["*/" "content"]]

didibus09:05:43

It's dirty, but:

(let [seen (atom 0)
      skip (atom false)]
 (partition-by
  #(do
    (when (= % ["*"])
     (swap! seen inc))
    (cond
     (true? @skip)
     (do (reset! seen 0)
      (reset! skip false)
      true)  
     (= 1 @seen) 
     true
     (> @seen 2)
     (do (reset! skip true)
      false) 
     :else false))                    
  [["content"]
   ["*"]
   ["content"]
   ["*"]
   ["content"]]))
((["content"])
  (["*"] ["content"] ["*"])
  (["content"]))

zackteo09:05:31

Wow! Yeap that definitely works! Thanks everyone for your help and advice so far 🙂

didibus09:05:35

Then you can map flatten it as a final pass to get exactly what you wanted

zackteo09:05:03

mmmm right right!

zackteo09:05:15

For now, I think I'll take a break and think this through further later :thinking_face:

raspasov09:05:49

Ok, I believe this works:

(ns ss.experimental.comment-line-count
 (:require [net.cgrand.xforms :as x]))


(defn count-comment-lines []
 (let [source (slurp "/Users/raspasov/cpp")
       lines  (line-seq
               (java.io.BufferedReader.
                (java.io.StringReader. source)))]
  (transduce
   (comp
    (partition-by
     (fn [line]
      (cond
       (clojure.string/includes? line "/*") :start
       (clojure.string/includes? line "*/") :end)))
    ;partition so we can access previous elements
    (x/partition 2 1 (x/into []))
    (map
     (fn [[[?comment-start-line] ?comment-lines :as v]]
      (if (clojure.string/includes? ?comment-start-line "/*")
       ;return number of commented lines
       (count ?comment-lines)
       ;else, return vector unchanged
       v)))
    ;only the numbers
    (filter number?))
   ;sum all the commented lines
   +
   lines)))

raspasov09:05:20

(requires the https://github.com/cgrand/xforms lib to do one partition transducer trick …)

didibus09:05:33

Here's a regex approach:

(map second
 (re-seq
  #"[\s\S]*?(/\*[\s\S]*?\\\*)[\s\S]*?"
  "c++ code here
/* Some c++ comment
     id here on multoline. \\*
 c++ here too
 /* Another comment \\*"))
Returns:
("/* Some c++ comment\n     id here on multoline. \\*"
 "/* Another comment \\*")

didibus09:05:24

Then you can just:

(map count
 (map clojure.string/split-lines
  '("/* Some c++ comment\n     id here on multoline. \\*"
    "/* Another comment \\*")))
To get the count of lines per comments.

didibus10:05:38

(2 1)
And then the sum of it all:
(reduce + '(2 1))
3

zackteo11:05:26

I just realised why the task is so complicated. I thought if I use partition-all like so it would work

(->> (partition-by (some-fn #(str/starts-with? % "/*")
                              #(str/ends-with? % "*/"))
                     (str/split-lines (slurp "resources/test.cpp")))
       (partition-all 3 1)
       (map (fn [x]
              (if (str/starts-with? (ffirst x) "/*")
                x (first x))))
       (map flatten)
       )
But didn't consider the case of the content inside the comment + the end comment

zackteo12:05:21

alas if we are choosing to work with state .... this way works ...

(partition-by
    (let [comment-region? (atom false)]
      (fn [line]
        (cond
          (str/starts-with? line "/*") (do
                                         (reset! comment-region? true)
                                         @comment-region?)
          (str/ends-with? line "*/") (do (reset! comment-region? false)
                                         true)
          :else @comment-region?)))
    
    (str/split-lines (slurp "resources/test.cpp")))

zackteo07:05:55

Am not sure what kind of function should I be using. Do I use reduce or do I have to loop through the sequence ?

zackteo07:05:45

Okay I think I understand that reduce should work. But for more complicated cases like this it can be quite difficult to reason about :thinking_face:

noisesmith16:05:44

the thing that made reduce / fold click for me was realizing that it is a direct translation of a for loop across a collection, with the nuance that you pass an immutable value forward to each iteration (with reduced allowing for early exit). note that if you need multiple values to be carried across elements ("updated for each cycle of the loop") you can easily use a hash-map as the accumulator and pull out the interesting values on exit

Christian10:05:55

Hi, I´m new to clojure and trying to post a multipart/mixed http request and have tried several http clients(clj-http, http-kit and hato) but none of them seems to support content type mulitpart/mixed. Is there any other client libraries that support mulitpart/mixed? So that we can to something like this. (The following sample returns an 415 because hato requries a multipart/form-data header. )

(defn multipart-request
  []
  (hc/request {:method "POST"
               :url my-url
               :headers {"cookie" my-auth-token 
                         "content-type" "multipart/mixed; boundary=batch_B01"}
               :multipart [{:name "request1"}
                           {:content-type "application/http"}
                           {:content "GET A_Product(Product='123456') Accept: application/json"}]
               })
)
The rest api Im using, (Odata RESTful API, that supports batch processing ) requires this in the header: "content-type" "multipart/mixed; boundary=batch_B01" And a body like this.
--batch_B01
Content-Type: application/http

GET A_Product(Product='123456') HTTP/1.1
Accept: application/json


--batch_B01--
Any suggestions on how to solve this? Thanks :-)

Drew Verlee21:05:54

I'm not familiar with multipart/mixed http requests, but in my experience your response can only have one "content-type" in the header. So when i see you put it twice, i'm confused, can you explain provide the response in plain text that you hope to create? That would help me at least.

Drew Verlee21:05:28

oh thats interesting, it does have multiple content types.

Drew Verlee22:05:11

That would be for your server handling a multipart params request, not for posting one though.

Drew Verlee22:05:03

your saying the server returns a 415 because > The HTTP `415 Unsupported Media Type` client error response code indicates that the server refuses to accept the request because the payload format is in an unsupported format. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415 So you would need to allow that content type, that's up to that server's handler code and not related to the http client unless i'm off track.

Christian08:05:30

Thanks for your response drewerel. We could just build our request without using the multipart part of the client http library, since we are able to make it work by building the request body "manually". But we want to test if multipart sections of a http client library can help us. Like described here https://github.com/gnarroway/hato#multipart-requests. Since it seems like Hato only supports content-Type of multipart/form-data, we may try to find another client library. So my question is really, has anyone tried to create a multipart/mixed request like the sample I posted, by using the multipart section of a http client library? Thx

Juλian (he/him)10:05:20

shouldn't :name, :content-type and :content all be part of the same map?

zackteo23:05:01

Hello, I have a function that gives me something like ([1 2 3] [4 5 6] [7 8 9]) where I want to know the sum of each column. So I use (apply map +) for that. However, if I get an empty list () apply will return a function. Which my later destructing does not handle. May I ask what is an idiomatic way to handle this? I thought of something like fnil but that doesn't really works here

craftybones02:05:43

You could use a (partial reduce + 0) which will give you a zero if you have an empty sequence in the coll

seancorfield23:05:18

@zackteo So you have (apply map + cols) ?

seancorfield23:05:20

What do want the result to be?

zackteo23:05:25

but (apply map + ())` will return a function

zackteo23:05:56

in most cases I will have (apply map + [[1 2 3] [4 5 6] [7 8 9]]) something like that

seancorfield23:05:53

Depending on what result you want back, maybe (apply map + (or (seq cols) [[]])) ?

zackteo23:05:01

I guess I'm thinking of how to do error handling, where I get an empty list. Perhaps I should be handling that earlier :thinking_face:

seancorfield23:05:49

Unless there is some obvious, neutral result for the “sums of no columns”, you should check cols before trying to apply map + I think.

zackteo23:05:45

I see I see!

zackteo23:05:41

How might I want to check this then? with an if ?

seancorfield23:05:46

dev=> (for [cols [ () '([1 2 3] [4 5 6] [7 8 9]) ]]
 #_=>   (apply map + (or (seq cols) [[]])))
(() (12 15 18))
if () is the “obvious, neutral result”