Fork me on GitHub
#clojure
<
2021-02-16
>
Illia Danko10:02:27

Hello there. Two weeks ago or so, I saw the topic about approaching different main application's targets using tools.deps somewhere (reddit or here don't remember). Mainly it's about passing a function name and the function's arguments to cli. Can anyone remember the title of the article/post?

Illia Danko10:02:36

Not sure is it the right channel to ask, apologists if isn't

p-himik10:02:45

Not sure whether this is what you meant but here's the documentation of the feature: https://clojure.org/reference/deps_and_cli#_executing_a_function

Illia Danko10:02:34

Thanks, as I remember it isn't about manipulating aliases or deps.edn, it's a contrived approach to run any public func from any namespace with just passing the function name, and it's arguments to CLI. And it was just ~10 lines of Clojure code.

Illia Danko11:02:21

Interesting, this approach I was looking for, but it was referred by some other source.

Schpaa10:02:53

How would you go about and write this using comp for the predicate instead of a fn?

(->> (:docs result)
     (remove (fn [{:keys [ref]}] 
                 (let [[_ r] ref]
                   (or (= r "cache")
                       (not= "fake-" (subs r 0 5))))))

Schpaa10:02:10

I’m just curious…

tvirolai11:02:38

Maybe like this:

(->> (:docs result)
     (remove (comp #(clojure.string/starts-with? % "fake-") second :ref)))
Checking for (= r "cache") seems redundant. I think using a plain predicate function like in your example is more readable than this kind of reversed pipelines.

vemv11:02:20

Yes it's particularly unfortunate that ->> goes in one direction and comp in the opposite It's trivial though to implement a custom comp that also reads left-to-right

p-himik12:02:24

IMO when you end up using a lambda fn within comp, it's simpler to just use a single lambda fn and do all the work there.

💯 3
p-himik12:02:03

Also, you can combine destructuring: (fn [{[_ r] :ref}] ...).

😀 3
💯 3
Schpaa12:02:11

Thanks for the input folks!

Schpaa12:02:28

I think the thing I was curious about was how comp would compose, so to speak, with or and and etc. I completely agree that it is more readable with a lambda, and that would be the most important thing.

vemv12:02:40

some-fn and every-pred are the 'or' and 'and' for these cases

Schpaa12:02:59

ah, interesting!!

p-himik12:02:25

Do you really think that

(comp #(clojure.string/starts-with? % "fake-") second :ref)
is more readable than
#(-> % :ref second (clojure.string/starts-with? "fake-"))
I just don't see that. The order is wrong, the string is longer, the % is buried.

p-himik12:02:53

Ah, you edited your message, I see.

p-himik12:02:33

Huh, usually Slack adds the edited flag to such a message. Or am I imagining things.

Schpaa12:02:46

I think you are imagining things right now

Schpaa12:02:01

Doesn’t matter, I get your point.

p-himik12:02:26

Nope, it definitely used to be the case. :) Anyway, yeah.

Yehonathan Sharvit13:02:01

I need some help with the formulation of a doc string

(defn concat-while
  "Returns a sequence of the concatenation of the elements of the collections in `coll-of-colls`
  while the number of elements in the sequence is less or equal to `n`.
  When we reach `n` elements keep concatenating the current collection and stop the concatenation.
  "
  [n coll-of-colls]
  (persistent! (reduce (fn [acc c]
                         (if (>= (count acc) n)
                           (reduced acc)
                           (into! acc c)))
                       (transient [])
                       coll-of-colls)))

(defn into!
  [to from] (reduce conj! to from))

noisesmith14:02:48

not what you asked, but unless I'm reading it wrong, this is a verbose way to write (into [] (comp cat (take n)) coll-of-colls)

noisesmith14:02:18

into already does transients, and cat already does the concatenation

noisesmith14:02:07

oh the difference is that it limits around collection boundaries, not an absolute count of the result:

(ins)user=> (concat-while 10 (repeat (range 3)))
[0 1 2 0 1 2 0 1 2 0 1 2]
(ins)user=> (into [] (comp cat (take 10)) (repeat (range 3)))
[0 1 2 0 1 2 0 1 2 0]

Yehonathan Sharvit13:02:01

Example:

(concat-while 12 (repeat 10 (range 5))) ; => [0 1 2 3 4 0 1 2 3 4 0 1 2 3 4]

Yehonathan Sharvit13:02:36

I am looking for a clearer formulation of what the function calculates

andy.fingerhut13:02:26

This might not be better, but perhaps something like: "Like (apply concat coll-of-colls), except ignore all elements of coll-of-colls after the result contains at least n elements."

ghadi14:02:14

Why this formulation and not take + concat?

ghadi14:02:34

or take + cat transducer?

gcaban14:02:58

I personally find the function name confusing and perhaps that’s one of the reasons why it’s hard to write a doc string for it. I’d expect concat-while to be equivalent off

(take n (apply concat coll-of-colls))
this function could be called concat-until-at-least-n-results or something like that and I’d be ok with a such a long name, because it’s something fairly specific that you’re doing here.

gcaban14:02:06

The logic is not the same as take + concat, but it took me a moment to realise it too. Thanks for proving my point @U050ECB92 😉

ghadi14:02:54

How is the logic different? I didn’t notice

gcaban14:02:32

the sample output is 15 elements, not 12, right?

noisesmith14:02:28

yeah - that tripped me up too, the count limit is only imposed at subcollection boundaries, not on the element count

ghadi14:02:26

yeah the example is not helpful because all the colls are the same range

noisesmith14:02:02

in fact, that trickiness is the real job the doc string should be addressing :D

6
gcaban14:02:13

and the function name! concat-while suggests this is a simple, generic combinator, while this is clearly something fairly application specific and imho that should be reflected in the name.

noisesmith14:02:01

how about chunked-concat

noisesmith14:02:27

except that might be confused with seq chunking so that's out

flowthing14:02:18

(defn halt-at
  [n]
  "A transducer that ends transduction when the result has n or more elements."
  (fn [rf]
    (fn
      ([]
       (rf))
      ([result]
       (rf result))
      ([result input]
       (if (>= (count result) n)
         (reduced result)
         (rf result input))))))

(transduce
  (halt-at 12)
  concat
  (repeat 10 (range 5)))

;;=> (0 1 2 3 4 0 1 2 3 4 0 1 2 3 4)
🤪

noisesmith14:02:22

I like that it's a transducer, but the doc string is false

noisesmith14:02:55

it terminates based on read-count not write-count, based on input count not result count

andy.fingerhut14:02:19

apply-concat-at-least ?

flowthing14:02:31

Oh yeah. :thumbsup::skin-tone-2: I’ll admit that aped the docstring from a core function without thinking about it too much.

Yehonathan Sharvit16:02:30

apply-concat-at-least is not accurate. Maybe concat-at-most but it doesn’t convey the fact that the result might contain more than n elements

Yehonathan Sharvit16:02:14

chunked-concat is my favourite for now

Yehonathan Sharvit16:02:55

@U4ZDX466T what does rf stand for in your custom transducer?

noisesmith16:02:38

it's a standard parameter name for the arg to a transducing function (see for example the one arity clause in (source map)

flowthing16:02:41

reducing function, I based the thing on halt-when from core.

kwladyka15:02:05

I have a API response which is not described and I have to make spec for this. Do we have any tool which show difference between huge spec nested structure and data which are not in spec? In very simple example: {:a 1 :b 2} Let’t say I have only :a in spec, but not :b. I want to know about that. In that way I can get all API responses and run against spec and see which values I have to add.

andy.fingerhut15:02:53

spec does not encourage creating "closed" specs, i.e. ones where any keys that aren't explicitly enumerated cause a spec failure, but you can still create such specs if you wish, e.g. there are some examples linked from this article (I haven't read the to see how effective they are): https://blog.taylorwood.io/2018/10/15/clojure-spec-faq.html

andy.fingerhut15:02:28

If you create such "closed" specs they should always give you failures whenever a key appears that you have not explicitly allowed.

andy.fingerhut15:02:14

(at the article I linked, search for the word "closed")

kwladyka15:02:19

I need this functionality only for developing purpose.

andy.fingerhut15:02:07

Understood. You can make them closed while developing, and when you believe you are done defining all of the keys you expect, you can make them open again.

👍 3
kwladyka15:02:58

BTW nice FAQ

andy.fingerhut16:02:03

or leave them closed -- it is up to you what you prefer, really. The Clojure maintainers have mentioned in talks on spec that usually they find when people create a closed spec, they later wish they hadn't, as they expand their information model.

kwladyka16:02:36

it will be open, but it should make start easier.

noisesmith16:02:34

a gotcha I somehow never discovered until now:

(defn gcd
  [a b]
  (loop [a (max a b)
         b (min a b)]
    (let [r (mod a b)]
      (if (zero? r)
        b
        (recur b r)))))
this is broken because loop inits use the same scoping rules as let

raspasov20:02:48

Not sure about other editors, but I find the Cursive highlighting indispensable for many “complex” cases like this one: Screen Shot 2021-02-16 at 12.47.02 PM.png

raspasov20:02:21

It highlights the “logical thread” of uses of “a”

raspasov20:02:55

Here it highlights the first two “a”, and stops there

noisesmith16:02:42

I think I only discovered this because it's not my clojure style, but a line by line rewrite of a low level program (for prototyping / debugging)

dpsutton16:02:14

the b (min a b) will use the a (max a b) and the b from the function args? so a and b could be equal?

noisesmith16:02:16

right, as written if a is smaller than b, it takes mod of the number itself

noisesmith16:02:59

I can't think of a use case for reusing loop bindings in further init clauses, but in terms of simplicity just using the same rules as let makes sense

noisesmith16:02:42

I could have skipped the loop form altogether and just did the min/max every time even though it's only possible for them to be out of order on the initial call

Alex Miller (Clojure team)17:02:04

loop and let are literally the same code in the compiler

👀 3
‼️ 6
Alex Miller (Clojure team)17:02:55

as in, there is no LoopExpr, only LetExpr with an "isLoop" flag

noisesmith18:02:02

reminds me of how bare minimum scheme implementations expand let into a lambda with an inline arg provided for each binding

Lennart Buit21:02:07

or how Haskell’s do syntax is syntactic sugar for bind >>=

Lennart Buit21:02:36

In some sense this is also true in test.check, which has gen/let which is actually implemented as gen/bind