Fork me on GitHub
#clojure
<
2024-04-01
>
igrishaev12:04:47

I'm a bit confused:

(def foo.bar 123)
#'user/foo.bar
but
foo.bar
Syntax error (ClassNotFoundException) compiling at ...
foo.bar
Surely I can skip dots in the name but why does (def) allow me do use them?

p-himik12:04:11

Because Clojure doesn't check for a lot of things, GIGO.

p-himik12:04:17

One of my favorite examples:

(clojure.set/intersection #{1 2} [2 3])
=> #{1}

Omer ZAK12:04:26

If you write user/foo.bar, you get the value and no syntax error. I tested in clj whose version is:

$ clj --version
Clojure CLI version 1.11.1.1429

p-himik12:04:51

^ that doesn't mean it should be used or is supposed to work that way.

Omer ZAK12:04:27

What is the official role of dots in names?

igrishaev12:04:33

By the way, quoting dotted symbols neither works:

`aaa
user/aaa

`foo.bar
foo.bar

p-himik12:04:05

> '.' has special meaning - it can be used one or more times in the middle of a symbol to designate a fully-qualified class name, e.g. java.util.BitSet, or in namespace names.

p-himik12:04:13

@U06BDSLBVC6 BTW that version is the CLI version, not version of Clojure itself. But doesn't really matter here.

Omer ZAK13:04:47

https://clojurians.slack.com/archives/C03S1KBA2/p1711976053093099?thread_ts=1711975547.236739&amp;cid=C03S1KBA2 This is the version of Clojure which is installed in my PC. Projects are, of course, free to require their own Clojure versions in their deps.edn or project.clj files.

p-himik13:04:47

That is the version of the Clojure CLI that's installed on your PC. The Clojure-the-language version will be different. Clojure-the-language doesn't even use commit-level versions. It's just three digits and an optional qualifier, something like 1.11.2 or 1.12.0-alpha3. This is how you can check the Clojure version that's used by your CLI on your particular machine (that can have settings that alter that version):

$ clj -M -e '(clojure-version)'
"1.11.1"

Omer ZAK13:04:18

I ran your code:

$ clj -M -e '(clojure-version)'
"1.11.1"
So clj version is 1.11.1.1429 and it runs Clojure version 1.11.1. Looks like there is some overlap in version designations, to say the least.

p-himik13:04:21

As I said, that can easily be changed on a global level. So by itself clj --version doesn't tell you the version of the language that will be used.

$ file deps.edn
deps.edn: cannot open `deps.edn' (No such file or directory)
$ clj --version
Clojure CLI version 1.11.1.1435
$ clj -M -e '(clojure-version)'
"1.9.0"

p-himik13:04:17

The three first parts of the CLI version do tell you the Clojure version that will be used by default, if there are no other configurations and no potential stale cache issues (no clue whether it's something relevant ATM but I'm pretty sure in the past there used to be some bugs with cp caching). But if you give someone else that version without any accompanying information, they cannot robustly infer the Clojure version that's been used.

šŸ‘ 1
seancorfield19:04:18

(~/clojure)-(!2002)-> clj --version
Clojure CLI version 1.11.2.1446

Mon Apr 01 12:29:56
(~/clojure)-(!2003)-> clj -M -e '(clojure-version)'
"1.12.0-alpha9"
šŸ™‚

JoĆ£o Galrito13:04:46

Hello, I'm trying to implement a flow control mechanism in a data processing pipeline that could let through or delay a message based on a condition (typically a target update rate) - what clojure concepts should I be looking at to implement this, in terms of communication between the different stages? I was thinking transducers but only because I might not understand them very well yet, I don't see examples of them being used in an asynchronous/decoupled from input fashion. The way I envision it being used is something like:

( source
       (filter ...)
       (map ...)
       (throttle 1000)
       output)

p-himik13:04:11

That sounds very vague so I can't give any concrete advice. Maybe core.async will be of use to you, channels let you implement throttling. And the overall description of the problem sounds cohesive with what core.async offers.

JoĆ£o Galrito13:04:27

ok, any pointers in how they can be fit together in a declarative way like in my pseudocode example? https://clojure.org/guides/async_walkthrough This jumps from a very surface overview straight to API docs (which tend to be a bit obscure) and source code

JoĆ£o Galrito13:04:07

or should I just read through all the core.async functions? šŸ™‚

JoĆ£o Galrito13:04:55

or is the magic in the <???> part to connect the parts using something like pipeline?

p-himik13:04:20

There should be some guides on core.async, but I'm not familiar with them so can't suggest anything concrete. The API has functions that let you provide your own transducers. But throughput/backpressure is something else. I think I saw an article once showing how to implement a throttler on top of a channel in a few lines, but I don't recall where.

JoĆ£o Galrito13:04:15

Ok thank you, I will look for more resources about core.async

JoĆ£o Galrito13:04:31

My only doubt is that I have the impression that transducers only work synchronously but maybe thatā€™s not the case

p-himik13:04:11

Transducers are a way to create a reducing function, that's it. That function can then be used in any context. Of course, there's a caveat about stateful transducers.

JoĆ£o Galrito13:04:21

I might be confusing them with the simple cases of map and filter for example

jjttjj13:04:29

For some reason I took your throttling to mean "batch by ms" and did this. If you just mean "drop repeated puts in that time span" you can maybe just use a dropping buffer

(defn throttle [ms out in]
  (a/go-loop [batch []
              timeout (a/timeout ms)]
    (a/alt!
      timeout ([_]
               (a/onto-chan! out batch false)
               (recur []
                      (a/timeout ms)))
      in ([x]
          (recur (conj batch x)
                 timeout))))
  out)


(def in (a/chan 10
          (comp (filter even?)
                (map str))))
(def out (a/chan (a/sliding-buffer 10)))

(throttle 1000 out in)

(dotimes [x 10]
  (a/put! in (rand-int 10)))
(a/poll! out)

p-himik14:04:50

If you enter that URL into the search bar here, you'll find multiple discussions with other approaches and library suggestions.

jjttjj14:04:43

You can also check out pipeline functions in core async to parallelize the transducers https://clojuredocs.org/clojure.core.async/pipeline

JoĆ£o Galrito14:04:38

alright, thank you both šŸ™‚ I will look into your suggestions, appreciate it

JoĆ£o Galrito14:04:41

@U064UGEUQ my main difference from your example is that I want to hold messages to be released later in some cases. I wanted to implement this using transducers so that they could compose neatly with filter and map, but I'm not seeing a way to do this since I need something like an external go-loop that has access to the output channel, and that channel is only available inside the call to the transducer function returned from my throttle transducer

jjttjj14:04:15

What do you mean by: > hold messages to be released later in some cases What cases exactly?

JoĆ£o Galrito14:04:23

ok, so my messages have a key and timestamp, and I want to release them independently per key at a target update rate. So if my target update rate is for example 5s, if a message comes for a particular key that is <5s after the previous one, I want to hold it, and release it later if no messages arrive until the 5s expire for that key

JoĆ£o Galrito14:04:16

if multiple messages come within the 5s window, I will only keep the latest one and release it when 5s have elapsed since the last message released for that key

JoĆ£o Galrito14:04:40

if a new message arrives that is >5s after the last one, it is immediately released (and stored in a hashmap for timestamp comparison with the next one)

jjttjj15:04:20

The https://github.com/cgrand/xforms library should have some useful stuff. I think you can use a combination of its x/by-key and x/window-by-time transducers to help get what you're looking for, I think.

jjttjj15:04:54

or x/reductions if all else fails

JoĆ£o Galrito15:04:07

alright, I'll take a look, thanks

jjttjj15:04:21

Though I wouldn't rule out just trying it with a go loop either, sounds like you could code it up pretty directly as one

JoĆ£o Galrito15:04:05

yea, that was what I was trying but I don't see how I can access the out channel inside the go-loop without explicitly creating it and passing it around, which will make me lose the ability to neatly compose throttle with other transducers (I think?)

p-himik15:04:13

If you decide to create a transducer, you must not use core.async. Not "cannot" but "must not". Because go blocks use threads from a very limited thread pool. Randomly waiting in such threads will exhaust the pool and bring your whole app to a halt.

p-himik15:04:56

If you want any abstraction mechanism and not necessarily transducers, you can build it on top of core.async with your own functions. Most functions will create transducers and call pipeline-blocking. Some functions, like throttle, will use pipeline-async.

JoĆ£o Galrito15:04:03

yes I just read that somewhere šŸ˜ I'm still trying out stuff so that's why I'm using go but I will take that into account

jjttjj15:04:16

My interpretation was that he's not waiting on stuff, just "caching" messages by some id and then evicting them after some amount of time has elapsed

JoĆ£o Galrito15:04:14

I'm building this stuff to be used by non-clojure developers in a Java interop context, so I want to get the usability/interface of it as simple as possible. That's why I wanted to make it similar/work seamlessly with filter and map when describing a sequence of operations to be performed on the data

p-himik15:04:13

You can still do it. Your namespace can have two functions, add-transducer and add-throttling.

JoĆ£o Galrito15:04:48

ok, but sounds like it's not trivial or desirable to treat them the same inside my coordination/composition internals, i.e. the user doesn't have to know or care what is or isn't a transducer

JoĆ£o Galrito15:04:58

(<???>
       (filter ...)
       (map ...)
       (throttle 1000 ...)
       (map ...))

JoĆ£o Galrito15:04:18

this is the interface I would like

p-himik15:04:12

You can do it by making (throttle ...) return some wrapper that's not a function and then use it to call pipeline-async.

JoĆ£o Galrito15:04:04

so I would just dispatch on that inside <???> ?

p-himik15:04:22

I have something like this in mind, haven't tested though:

(defn throttle [ms]
  {:async-handler (fn [value chan]
                    ;; Do whatever needs to be done to put
                    ;; `value` onto `chan` at the right time.
                    )})

(defn build-pipeline [source n
                      & steps]
  (let [bundled-steps (into []
                            (comp (partition-by fn?)
                                  (mapcat (fn [bundle]
                                            (if (fn? (first bundle))
                                              [(apply comp bundle)]
                                              bundle))))
                            steps)]
    (reduce (fn [from step]
              (let [to (a/chan)]
                (if (fn? step)
                  (a/pipeline-blocking n to step from)
                  (a/pipeline-async n to {:async-handler step} from))
                to))
            source bundled-steps)))

(let [source (a/chan)
      dest (build-pipeline source 10 
                           (map inc)
                           (filter even?)
                           (partition-all 3)
                           (throttle 1000))]
  ...)

JoĆ£o Galrito15:04:59

ok I think I understand this better now, thank you both

JoĆ£o Galrito15:04:07

what is the difference between pipeline-blocking and pipeline btw?

p-himik16:04:00

No difference.

craftybones15:04:17

Stupid question I suppose. Is there a way to get the metadata of a var if its value is passed onto a function? Passing the var is the obvious way I suppose

(defn foo {:data 5} [x] x)

(defn some-other-fn [f]
  ;; need metadata of foo(passed via f) here
  )

(some-other-fn foo)

craftybones15:04:33

I remember a particular arcane longwinded way of demunging or something that someone once mentioned ages ago. I remember laughing at it considering it as being insane.

p-himik15:04:35

No way to do it without using the var itself. Either by using #' (or var) or a macro. Demunging might be somewhat useful only for things like functions, such as in your example. But not for vars in general since e.g. 7 is a plain number, there's nothing to munge.

craftybones15:04:59

Yeah, not vars in general. In my case it is a fn.

craftybones15:04:17

Ooo, a macro.

p-himik15:04:37

Oh, BTW - in your example, that metadata is not on the var but on the args vector. So you can't access it at all.

craftybones15:04:54

Yeah made a mistake. I forgot to remove the caret.

craftybones15:04:14

Things that often begin with ā€œooo macroā€ donā€™t often end well.

šŸ˜„ 3
craftybones15:04:29

Thanks anyway. Iā€™ll find a way around

p-himik15:04:51

One thing worth noting is that metadata might be a poor fit for what you want to do anyway. But, of course, it depends. Maybe it is a good fit. Maybe an explicit registry is a better fit. Maybe some introspection. And so on.

craftybones15:04:28

Iā€™m trying to imitate some system someone else has built using Java annotations(ugh). They have all this annotated data that they use to make runtime decisions.

craftybones15:04:28

Youā€™re right about the registry, which is sort of what Iā€™d shown them originally. Just wanted to bring it closer to their idiom I suppose. Not a big deal

p-himik15:04:23

A custom function-defining macro is much closer to Java annotations than plain metadata. :)

craftybones15:04:00

Actually, Iā€™d prefer something like reitit or malli.

craftybones15:04:24

But yeah, a macro system for sure.

phronmophobic15:04:42

caveat emptor, ymmv, here be dragons, etc.

Filipe A.19:04:55

Hi, I found something interesting. Could someone clarify it for me? Why call to write it writes an empty file, but it writes "foo" as expected using with-open?

(.write ( "/tmp/file") "foo") ;; writes empty file
(with-open [wtr ( "/tmp/file")] ;; writes foo as expected
  (.write wtr "foo"))

p-himik19:04:20

You never close in the first case - the output isn't flushed, I would assume. Try the same but with exiting the process.

p-himik19:04:27

Or with calling (.flush the-writer) explicitly (`doto` is helpful here). But in any case, you should of course be using with-open if a writer has a well-defined scope and you don't need to also read from that file at the same time.

Filipe A.19:04:54

Oh yeah! Flush, I missed that in my mental model for sure I was curious why first it was not working. Thanks for clarifying it for me

šŸ‘ 1
Filipe A.19:04:01

When I created a BufferedWriter, the bytes were written to the buffer and not transmitted to the file system.