Fork me on GitHub
#clojure
<
2022-04-26
>
flowthing06:04:13

user=> (clojure.lang.Compiler/load (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. "(")))
Syntax error reading source at (REPL:2:1).
EOF while reading, starting at line 1
user=> (-> *e Throwable->map :via first :data)
#:clojure.error{:phase :read-source, :line 2, :column 1}
Why does Clojure report that the error is on line 2, when it's actually on line 1?

p-himik06:04:12

> :column 1 My immediate assumption, without checking, is that the last character is \newline.

flowthing06:04:18

That's my assumption as well, but I guess I don't know where that newline is. 🙂 There's no newline in the string I'm reading.

p-himik06:04:34

Ah, I failed to read the line you feed to the REPL correctly - I thought it's your line that had an issue and not something you put in a string there.

hiredman06:04:59

I would try wrapping it in a function and calling the function

hiredman06:04:15

Or maybe not

flowthing06:04:26

Doesn't seem to make a difference.

hiredman06:04:27

The compiler uses dynamic bindings to pass around position information if I recall, and it maybe that the state there from the repl's call to eval is cross talking with the code being evaled

p-himik06:04:45

Or it might still be the issue of there being an implicit newline, so to speak:

(def r (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. "(")))
=> #'dev/r
(.getLineNumber r)
=> 1
(.read r)
=> 40
(.getLineNumber r)
=> 1
(.read r)
=> -1
(.getLineNumber r)
=> 2

flowthing06:04:58

Hmm, maybe. I can call .setLineNumber on the LineNumberingPushbackReader, but the result is still confusing:

user=> (let [reader (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. "("))]
  (.setLineNumber reader (int 0))
  (clojure.lang.Compiler/load reader))
Syntax error reading source at (REPL:1:1).
EOF while reading, starting at line 0

flowthing06:04:25

Oh, that's probably it. :thumbsup:

flowthing11:04:39

It doesn't feel quite right to me, though. :thinking_face:

flowthing11:04:12

To kinda interpret every string as ending in a newline, that is. I haven't yet looked at the code for LineNumberingPushbackReader to see what's going on there, but this is confusing:

user=> (clojure.lang.Compiler/load (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. "(\n")))
Syntax error reading source at (REPL:2:1).
EOF while reading, starting at line 1

flowthing11:04:01

So this is interesting:

λ java -version
openjdk version "11.0.2" 2019-01-15
OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)
λ clj -Srepro -M -e '(clojure.lang.Compiler/load (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. "(defn square [x] (* x x)")))'
Syntax error reading source at (REPL:1:1).
EOF while reading, starting at line 1

Full report at:
/var/folders/yb/qchyz_mx5w97ncww09ffrfz00000gp/T/clojure-17057255385196411412.edn

λ jabba use [email protected]
λ java -version
openjdk version "17" 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
λ clj -Srepro -M -e '(clojure.lang.Compiler/load (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. "(defn square [x] (* x x)")))'
Syntax error reading source at (REPL:2:1).
EOF while reading, starting at line 1

Full report at:
/var/folders/yb/qchyz_mx5w97ncww09ffrfz00000gp/T/clojure-6224778322342884943.edn

flowthing12:04:53

Good find! :thumbsup:

flowthing12:04:48

Yep, case closed, I think. Thanks for the help. 🙂

👍 1
Abhinav09:04:30

Hi, I’m using https://github.com/BrunoBonacci/mulog, Is there a way to disable just tracing (not logs) in a specific environment? thanks

bruno.bonacci10:04:06

Hi @U02CVMEFEUF I'm the author of μ/log. Trace events are just plain events so they can be filtered using the https://cljdoc.org/d/com.brunobonacci/mulog/0.8.2/doc/custom-transformations which all the built-in publishers support. the basic usage is:

(u/start-publisher!
  {:type :console
   :transform (fn [events] events)})
for example if you wanted to remove a trace event called :app1/my-noisy-event you could do as follow:

1
bruno.bonacci10:04:51

(u/start-publisher!
  {:type :console
   :pretty? true

   :transform
   (fn [events]
     (remove #(= (:mulog/event-name %) :app1/my-noisy-event) events))})

bruno.bonacci10:04:56

Similarly, if you wanted to remove all trace events (which I discourange) you could remove anything which has a :mulog/duration field.

(u/start-publisher!
  {:type :console
   :pretty? true

   :transform
   (fn [events]
     (remove :mulog/duration events))})

bruno.bonacci10:04:44

Feel free to post a discussion in Github if you need something else: https://github.com/BrunoBonacci/mulog/discussions

Abhinav10:04:04

thanks a lot.

apt14:04:14

Hi folks. I’d like to check at runtime if a dependency version is greater than a certain number. This is required for the lib I’m working on, which is ‘injected’ in different projects with possibly different dep versions. I was thinking in using https://github.com/clojure/java.classpath. It would return a list of things like that:

#object[java.io.File 0x3ffcc967 "/Users/my-user/.m2/repository/com/datomic/datomic-lucene-core/3.3.0/datomic-lucene-core-3.3.0.jar"]
So I could simply get the file name and extract the part before the jar, like 3.3.0 above. However, I don’t know how this path is mounted and if I can rely on that. WDYT?

p-himik14:04:12

There's no robust way to get a version of a library, unless you control that library and put the version information there yourself.

apt14:04:29

> unless you control that library and put the version information there yourself. Supposing this assumption were true, how would I do it?

p-himik14:04:44

Just add a def into that library that has the version. :)

p-himik14:04:59

Exactly how we already have *clojure-version*.

apt14:04:09

Ah okay. Got it.

dpsutton14:04:38

but beware that you have to conditionally check for it since there are versions released without it. You’ll need to check for the presence of the var before actually using it

👍 1
noisesmith15:04:02

to tag onto @U11BV7MTK - the easy way to do that is resolve - returns nil if the symbol is not bound

(ins)user=> (resolve 'foo/bar)
nil
(ins)user=> (resolve 'clojure.core/+)
#'clojure.core/+

👍 1
fadrian15:04:02

I'm working on a large collection of objects which I have to map over as quickly as possible. To complicate matters, the function that is mapped over is of the form:

(map #(with-open [conn (make-connection)] (do-stuff conn %)) xs)
Simple, right? Use pmap or shove the items into a core.async channel with multiple worker threads, right? Well not quite... The problem is that (a) one can only have a certain number of connections open at a given time and (b) a connection opened on one thread cannot be used in a different thread. The multiprocessing in pmap can't be limited to the (low) number of connections allowed and pmap and core.async don't let one associate a given connection to a given thread. To complicate things further, opening and closing a connection is relatively expensive and so I'd like to minimize the number of connection opens and closes. Is there a good way to process the collection using a fixed connection pool where each pool item is associated with a particular thread that can be re-used?

p-himik15:04:58

Split the input into N parts, create a threadpool of size N, in each thread create a connection and process a single part sequentially, in the end combine all the results.

p-himik15:04:00

Unless you need for it to work in a lazy manner.

p-himik15:04:11

And with core.async, you can use pipeline with a set parallelism and a custom connection factory that would create and cache one connection per thread. After the work is done, you'd get rid of the factory and all the cached connections.

p-himik15:04:05

Also, perhaps https://github.com/clj-commons/claypoole could be of help but I haven't used it myself.

fadrian16:04:05

I looked into claypoole this morning. I may end up using that. I was hoping for something a bit more simple. Thanks for the pipeline suggestion, too.

Peter Morris15:04:32

Is there anyone here who also has .net installed? I'm interested in how Clojure the immutable list class performs compared to the .net implementation. I've written some benchmarks... https://gist.github.com/mrpmorris/8b692d62e54bff035259d31d2665bc26

Peter Morris15:04:10

Does anyone fancy trying this out in Clojure so we can compare performance?

Peter Morris15:04:25

From what I have read, Clojure should be much faster

p-himik15:04:19

Clojure is fully crossplatform - you can compare yourself, and it would make more sense because it would be working on the same machine.

Peter Morris15:04:56

I'm hoping to find someone curious to give it a try with me

Peter Morris16:04:17

Then nobody needs to know both

p-himik16:04:43

Yeah, but the results won't be comparable, because the machines will be completely different.

Peter Morris16:04:31

Both benchmarks would be run on the same machine

p-himik16:04:59

Ah, I see what you mean.

p-himik16:04:26

Although seems like a Clojure equivalent to a list in C# would be a vector and not a list.

Peter Morris16:04:29

I've done the .net side, and was hoping to find someone curious like myself to write the equivalent Clojure code

Peter Morris16:04:52

It's an ImmutableList, so basically a Persistent Data Structure that acts like a list

p-himik16:04:32

I get that. I meant from the sensible API point of view. In C#, a list is a collection that supports random access. In Clojure, that would be a vector, even though it implements Java's List.

Peter Morris16:04:55

Is it immutable?

Peter Morris16:04:50

In .net, ImmutableList you can randomly access by index, and when you want to Add it gives you a new object instance for the ImmutableList with the value added. Internally it uses a binary tree I think

Peter Morris16:04:04

I have read that Clojure uses an n-ary tree

Peter Morris16:04:11

Which is why I am interested in comparing the speeds

p-himik16:04:25

I can write Clojure code that would do what you're doing in the "ImmutableList<T> benchmarks" section, but I can't run the .NET code and I can't promise that the result will be 100% equivalent. E.g. I.d implement "remove at zero" as creating a subvector in Clojure - I have no idea what .NET does here, whether it creates a fully new list or not.

p-himik16:04:54

I don't know a thing about .NET, so I have to ask - what's the difference between (l, x) => l.Add(x) and (l, x) => l = l.Add(x)?

Peter Morris16:04:10

Ah, because I am using the same code to do the loop I am passing an functions to do things like 1: Create the instance (List, ImmutableList, whatever) 2: To add values 3: To remove values

Peter Morris16:04:30

1: How long does it take to add numbers sequentially 2: How long does it take to end up with an empty list when removing the last element (position length - 1) 3: When removing the first element (position 0) 4: When removing an element with a specific value that will be found at the start of the list 5: When removing an element with a specific value that will be found at the end of the list

Peter Morris16:04:08

So 1,2,3 are about mutations. 4 and 5 are about traversing the collection to find a single element

didibus16:04:00

You should benchmark iteration as well, how quickly you can iterate over it

p-himik16:04:26

It's still more confusing to me that it could (or maybe even should) be. 1 and 2 - alright, got it. 3 - just as in 2, till you end up with an empty list? If so, Clojure's vectors are not optimized for that. Clojure's lists are proper linked lists - adding and removing items at the head makes the most sense. Clojure's vectors are n-ary trees indeed, where n is 32, and they're optimized for tail modification and random access. 4 and 5 - same as above, vectors aren't really for that. If that's something you need often, a different data structure should be considered. So, judging by just the .NET's List API, seems like comparing it to any single Clojure data structure would be comparing apples to oranges.

p-himik16:04:06

If so, it would be better to come up with scenarios that don't depend on any concrete type and then benchmark reasonable implementations of those scenarios, akin to Rosetta code.

Peter Morris16:04:37

Oh, I was under the impression that in Clojure it was optimised for both addition and removal. Is that not the case?

p-himik17:04:55

Addition - only at the tail. Removal - if a proper one, when the removed items can be garbage collected, then also only at the tail. But you can simulate removal with subvectors - those behave like vectors but retain a reference to the main vector. Lists are the reverse of that, with some additional differences, like not supporting fast random access and not having anything like subvectors.

didibus17:04:19

I don't think the ImmutableList of C# optimizes to all these either though. I think they just offer all the convenience functions no matter the performance. That's my guess at least.

didibus17:04:45

What does your test results for C# shows? Any one of them seems a lot slower than the other?

Peter Morris17:04:58

ImmutableList is only slow for finding values

Peter Morris17:04:10

Note I had to peform List.Add 100 times more to get a benchmark

didibus17:04:14

This is with what size list?

didibus18:04:38

;; Size of n to test for.
(def n 1000000)

;; 1: How long does it take to add numbers sequentially

;; 18.9 msec
(time
 (do (into [] (range n))
     nil))

;; 2: How long does it take to end up with an empty list when removing the last element (position length - 1)

;; 79.3 msec
(let [v (into [] (range n))]
  (time
   (loop [v v]
     (when (> (count v) 0)
       (recur (pop v))))))

;; 3: When removing the first element (position 0)

;; 80.7 msec
(let [v (into [] (range n))]
  (time
   (loop [v v]
     (when (> (count v) 0)
       (recur (subvec v 1))))))

;; 4: When removing an element with a specific value that will be found at the start of the list

;; 49.3 msec
(let [v (into [] (range n))]
  (time
   (do (filterv (complement #{0}) v)
       nil)))

;; 5: When removing an element with a specific value that will be found at the end of the list

;; 48.7 msec
(let [v (into [] (range n))]
  (time
   (do (filterv (complement #{(dec n)}) v)
       nil)))
That's on my machine, for n = 1000000 and without using a proper JVM warmup benchmarking solution, just a simple time measurement.

didibus18:04:11

There'd be ways to optimize some of those specific scenarios, like with the find case, if you find at the start or end you can more easily pop or subvec, where as here I went with the more generic, no matter where I find it, which means I have to recreate a new vector which is slower.

didibus19:04:40

Here it is with some of the more optimal ways that I could think of:

;; Size of n to test for.
(def n 1000000)

;; 1: How long does it take to add numbers sequentially

;; 18.9 msec
;; Already optimal
(time
 (do (into [] (range n))
     nil))

;; 2: How long does it take to end up with an empty list when removing the last element (position length - 1)

;; 79.3 msec
(let [v (into [] (range n))]
  (time
   (loop [v v]
     (when (> (count v) 0)
       (recur (pop v))))))

;; More optimal way
;; 28 msec
(let [v (into [] (range n))]
  (time
   (loop [v (transient v)]
     (if (> (count v) 0)
       (recur (pop! v))
       (persistent! v)))))

;; 3: When removing the first element (position 0)

;; 80.7 msec
;; Already optimal because Clojure subvec doesn't work on transient yet
;; see: 
(let [v (into [] (range n))]
  (time
   (loop [v v]
     (when (> (count v) 0)
       (recur (subvec v 1))))))

;; 4: When removing an element with a specific value that will be found at the start of the list

;; 49.3 msec
(let [v (into [] (range n))]
  (time
   (do (filterv (complement #{0}) v)
       nil)))

;; More optimal way
;; 1.3 msec
(let [v (into [] (range n))]
  (time
   (do (let [length (count v)
             i (.indexOf v 0)]
         (cond (= 0 i)
               (subvec v 1)
               (= (dec (count v)) i)
               (pop v)
               :else
               (let [v1 (subvec v 0 i)
                     v2 (subvec v (inc i))]
                 (into v1 v2))))
       nil)))

;; 5: When removing an element with a specific value that will be found at the end of the list

;; 48.7 msec
(let [v (into [] (range n))]
  (time
   (do (filterv (complement #{(dec n)}) v)
       nil)))

;; More optimal way
;; 24.4 msec
(let [v (into [] (range n))]
  (time
   (do (let [length (count v)
             i (.indexOf v (dec n))]
         (cond (= 0 i)
               (subvec v 1)
               (= (dec (count v)) i)
               (pop v)
               :else
               (let [v1 (subvec v 0 i)
                     v2 (subvec v (inc i))]
                 (into v1 v2))))
       nil)))

Peter Morris19:04:26

Do you have .net installed so could build + run some .cs benchmark code?

Peter Morris19:04:33

I am curious how it compares on the same machine

isak19:04:50

Not exactly what you are trying to do I think, but you may find it interesting. (Clojure map on CLR is to the right on the chart above)

didibus19:04:31

I don't have c# sorry

Carlo16:04:02

I'd like to get the source of a function I defined in my files (to do further analysis). I know about clojure.repl/source and clojure.repl/source-fn and how those are not a good fit for this usecase - since my functions are not in the classpath. I'm interested in what's the recommended way of getting the source of something in the library you're working on, or your advices if there isn't one

p-himik16:04:28

Not entirely sure what the intended use case is, but sounds like just reading the source file and finding the form that defines the right function by name would be a solution.

🙌 1
dpsutton16:04:33

It should be quite similar. The only reason that the classpath is relevant is that that’s how they find the resources/files. The source macro uses the var passed in to look up the var and get the namespace information. and namespaces must be findable on the classpath. If you come up with some other mechanism to give a file and symbol you can read the file for it. But now it is two bits of information instead of one (the var and from that the file)

🙌 1
dpsutton16:04:05

but i’m also confused if you are working on a library why it isn’t on the classpath

Carlo16:04:07

right, so here's the thing I'd like to build: sometimes, when I'm exploring a codebase which isn't mine, maybe because I would like to do a PR, I want to understand how the execution of a function works. So suppose I have:

(defn foo [a b] (inc (* a b)))
(defn bar [a b c] (+ (foo a b) (foo a c)))
I'd like to be able to type:
(bar 1 2 3)
and then positioning the point on the appropriate sexps, to expand them, by substituting in the function. So, a series of expansions could look like:
(bar 1 2 3)
(+ (foo 1 2) (foo 1 3))
(+ (inc (* 1 2)) (foo 1 3))
I care less about the result of any subform, and more about how this result is computed with the abstractions I have in the library.

Carlo16:04:15

@U11BV7MTK I mean, I can't do (source bar) in the example before, and I guess that is because the library I'm working on is not on the classpath, but only the dependencies (? please correct me if this is wrong)

Carlo16:04:19

@U2FRKM4TW yes that's possible, I kinda hoped someone knew of a solution that spared me some work; maybe rewrite-clj is what I should be reaching for (and using it just for parsing)?

dpsutton16:04:40

oh neat. it’s like macroexpansion but function expansion. I can think of lots of problems that you’ll have to fix (polymorphism comes to mind immediately, figuring out what protocol body might come through, multimethod body, reify, all of which are runtime values and not statically determinable). But this could be a really cool tool

dpsutton16:04:42

you’ll also get into figuring out variable scopes and shadowing when you substitute the actual arguments into the body

Carlo16:04:50

yes, it's exactly that, but with a bit of UI on top to let you choose what you want to expand (keeping macros in mind). For the parts that have "multiple choices" (polymorphism, protocols etc) I'd probably want the user to eval enough of the datastructure so that we know which path we want.

Carlo16:04:32

indeed, I'll need renaming. So I'm kinda interested of what kind of tools are already there to not duplicate effort

dpsutton16:04:47

the obvious go-to is clj-kondo which should handle all of this for you (the analysis). You’ll have to piece it back together though

🙌 1
Carlo16:04:00

thank you, I never looked into how much analysis clj-kondo is capable of doing. It could very well be all I need!

dpsutton16:04:42

it of course has an opaque macro barrier. so you’ve got your work cut out for you. But since you are making something like does expansion perhaps you can move past that barrier

Carlo16:04:01

what do you mean? which opaque macro barrier?

Carlo16:04:13

Oh I see, yes but I'm cool with expanding the macros I need myself - in fact I don't think I can avoid that work in the general case - for now I just want the source with as much AST annotation I can get, to help me with shadowing!

dorab16:04:18

What you're talking about sounds like symbolic evaluation. If so, typed Clojure might be a place to look as well.

🙌 1
Ben Sless17:04:27

Something else you could do is mess with the reader or with defn. If you could attach the form as metadata to a var definition, would that work?

🙌 1
Carlo19:04:07

@UK0810AQ2 I might consider that, but in practice I also need to keep in mind the ns declaration in which the function is defined (so that I can disambiguate the calls inside the function). So I'm searching more for a tool that can parse all of the file and do some correlated operations

Noah Bogart16:04:18

is there a community standard regarding defining protocols in a my.namespace.proto namespace with the implementation(s) other places, vs defining the protocols in a my.namespace namespace with the implementation(s) in my.namespace.impl namespaces?

p-himik16:04:02

FWIW I've seen both, can't really say which is more frequent. If I had that question, I'd approach it from the perspective of the end user - what's needed more often, the implementations or the protocols? And you might even end up with both m.n.proto and m.n.impl with m.n having some factory functions or something like that.

👍 1
Alex Miller (Clojure team)16:04:26

generally, as protos are interfaces, I find it avoids a lot of entangling if you put them in a separate namespace than the impls

👍 3
Alex Miller (Clojure team)16:04:43

but I've probably done all the options at one point or another :)

Noah Bogart16:04:43

right, i was more thinking if it's "better" to say (require '[my.namespace.proto :refer [cool-fn]]) or (require '[my.namespace :refer [cool-fn]]) . i know so much of this is just opinion and style, but i like to stick with the community standards when possible

Alex Miller (Clojure team)16:04:44

my other piece of advice is that it's often useful to front protocol functions with a wrapper function

Alex Miller (Clojure team)16:04:51

I know that seems like a lot more ceremony, but I have found that to be useful so many times now that I almost always just start that way

Alex Miller (Clojure team)16:04:15

then the function wrapper is the api, and the protocol function becomes more like an spi

Noah Bogart16:04:46

iiiiiinteresting, yeah

Noah Bogart16:04:07

how does that interact with extending a protocol? or i guess it doesn't? you still extend the protocol like normal but then call the wrapping function in practice

Noah Bogart17:04:06

idk if you've written about this elsewhere (applied clojure maybe? just got my copy yesterday lol), but i would love to hear more about the benefits you've encountered

Alex Miller (Clojure team)17:04:29

I'm not sure I had come to this when I wrote that yet, don't remember. don't think I've written about it anywhere, but it's something Rich and I have talked about and regularly do. spec (at least spec 2) is set up this way - there's a protocols namespace, then the main api which wraps and calls into the protocol functions, and then an impl (although that split is not necessarily interesting)

Alex Miller (Clojure team)17:04:21

it gives you a place to put defaults, aspect stuff like logging, adaptation into other protocols, etc

👍 1
Noah Bogart17:04:54

cool, thank you

seancorfield17:04:03

FWIW, it's a lesson I wish I'd learned before writing next.jdbc -- that mostly has the protocols separate, but I added some elsewhere in the library and it made both using the library and maintaining it more complex.

p-himik17:04:46

nnext.jdbc when? :)

laughcry 2
Ben Sless17:04:02

Design question related to component, but relevant to every case of building a system and DI. I'm using Component and Aero I would like to be able to build and implement my system such that different components' implementations could be run at development time and in production, without having to "massage" the production system to swap implementations completely and modify the dependency metadata. The biggest hurdle I came across is that one component I build based on a URI is built of a java class, but depending on the URI scheme I might need to use a completely different class. On top of that, in one case I need to take this data and use it to initialize another component, while on the other case I don't need that component to exist at all. Case in point: grpc channel builder, can be built for from URI string (with managed channel builder), but if I want an in process channel, I need to use another builder (in process builder)

Joshua Suskalo17:04:35

without much more context that kinda sounds like something polylith would solve

hiredman18:04:33

you can create component components that return something else when you call start on them

hiredman18:04:00

(defrecord ConfiguredChoice [configuration key]
  component/Lifecycle
  (start [this]
    (component/start ((get this (configuration key)) this)))
  (stop [this]
    (assert nil)))

Ben Sless18:04:33

In the end, this is only orthogonal to component / polylith, etc. When I have one scheme I want to use one builder, when I have another I want another and to do some side effect

emccue18:04:06

So you want a part of your system to conditionally depend on a thing based on whether or not it needs it for a given implementation

emccue18:04:17

and if that thing is not depended upon, you want it to not exist

emccue18:04:34

A -> B
     ^
    /
C -

A -> B'

hiredman18:04:47

ConfiguredChoice is something I've used in the past, I forget exactly when and why, maybe different storage backends or redis clients, which was for configuring behavior of deployed software

Ben Sless18:04:46

so both implementation will live in the same system but only one will get started?

hiredman18:04:34

it depends how you use configuredchoice

hiredman18:04:56

the configuredchoice component replaces itself with whatever choice it makes when you start it

hiredman18:04:31

an instance of configuredchoice looked like

(map->ConfiguredChoice
 {:key :storage-backend
  :mysql (fn [this]
           (mysql/map->Store (assoc this :db (:database this))))
  :local (fn [this]
           (log/info "using local storage")
           (store/->LocalStore))})

hiredman18:04:13

so once it is started it would be replaced by the result of one of those functions

hiredman18:04:23

the key is to understand that start and stop on components are not side effecting functions that return nil, they return "updated" versions of the component they are called on

hiredman18:04:53

and those updated versions can be whatever you want, they don't have to be the same type

hiredman18:04:33

for changing things in tests I usual just manipulate the system before starting it, it is a map so you can just add and remove whatever until it is what you want before you start it

Ben Sless18:04:19

That's what I'm doing now, I'm trying to outsmart myself

emccue18:04:58

So a few approaches here - you can have a factory take that URI and return B or B'

Ben Sless18:04:48

Annoying part here is that I don't have C->B but B->C but only if I have one URI scheme and not another

Ben Sless17:04:06

Managed to figure it out with both protocols and multimethods

emccue18:04:20

but both B and B' will depend on C

emccue18:04:39

...so one approach is to make a dummy no-op C

emccue18:04:50

that also creates itself based on the uri

Ben Sless18:04:14

So all implementation paths depend on the URI

emccue18:04:46

Or approach #3, have "create a C" be a logical method on B. Dummy B creates dummy C

emccue18:04:11

still ultimately dependent on the URI, just a little more detached

Ben Sless18:04:21

which makes me think this should be a single component

Ben Sless18:04:25

but I dislike it doing too much

emccue18:04:46

okay approach #2, get funky with it

emccue18:04:46

'{:URI "a:123"
  :B   (start-B (clip/ref :URI))
  :C   (start-C (clip/ref :B))}
Say we are using clip and this is our system map

emccue18:04:26

'{:URI "a:123"
  :B   (start-B (clip/ref :URI))
  :C   ^:orphan (start-C (clip/ref :B))}
slap a dash of metadata

emccue18:04:00

(defprotocol Wayne
  (orphan-children? [_]))

emccue18:04:04

make a protocol

emccue18:04:06

(extend-protocol Wayne
  Object
  (orphan-children? [_]
    false))

emccue18:04:10

make a default impl

emccue18:04:25

'{:URI "a:123"
  :B   (start-B (clip/ref :URI))
  :C   (when-not (orphan-children? (clip/ref :B))
         (start-C (clip/ref :B)))}
pre-process the map, crawling for refs

emccue18:04:39

and make your test B implementation go all Thomas and Martha on it

seancorfield18:04:28

Maybe 🧵 replies folks instead of hogging the main channel please?

🙏 7
1
1
Daniel Jomphe20:04:05

When wrapping a third-party lib, we need to: 1. Copy-paste the docstring of the wrapped fn, etc. 2. Update the docstring when we realize the underlying lib docstring changed. Can we instead reuse the original docstring, maybe concatenating a docstring prefix, for example? Have you found a way to do so that works well for you and your IDE's doc tooltips? Screenshots illustrate what we're doing currently, the full copy-paste...

Daniel Jomphe20:04:50

Here's a https://clojurians.slack.com/archives/C03S1KBA2/p1651005353239669 (which became its own thread with some building blocks). Still seeking a working solution.

seancorfield22:04:48

Calling (meta #'the-ns/the-fn) will give you the metadata and it will have a key :doc that you can apply to the wrapping function.

seancorfield22:04:08

There's a bit of machinery at the end of expectations.clojure.test that pulls in functions from clojure.test and reuses their metadata but adds "Imported from clojure.test" to the docstrings: https://github.com/clojure-expectations/clojure-test/blob/develop/src/expectations/clojure/test.cljc#L540-L560

seancorfield22:04:27

(it's complicated by catering for self-hosted cljs via planck as well as Clojure but...)

seancorfield22:04:51

In use:

user=> (require '[expectations.clojure.test :refer :all])
nil
user=> (doc run-tests)
-------------------------
expectations.clojure.test/run-tests
([] [& namespaces])
  Runs all tests in the given namespaces; prints results.
  Defaults to current namespace if none given.  Returns a map
  summarizing test results.

Imported from clojure.test.
nil

seancorfield22:04:12

user=> (doc clojure.test/run-tests)
-------------------------
clojure.test/run-tests
([] [& namespaces])
  Runs all tests in the given namespaces; prints results.
  Defaults to current namespace if none given.  Returns a map
  summarizing test results.
nil

rayat00:04:29

Dang, being just data keeps getting cooler the more I learn about it

Ben Sless05:04:16

Another way to deal with that could be something like Emacs's advice mechanism or middleware. Problem is I'm not sure how it would play with compilation and direct linking

Daniel Jomphe20:04:41

Finally came back to this. Thanks a lot, Sean. The intern-based solution worked. Observe the (defn wrap-fn solution works but Clj-Kondo underlines in red the produced var, obviously. Then, a lightweight (defmacro defwrapped wraps the call to make it understandable by clj-kondo. In this example, note it only allows to prefix and suffix docstrings.

Daniel Jomphe20:04:43

This current implementation allows us to give a new name to the wrapped function. Not sure if it's really a good idea. The most important next step is to weed out how to override the original fn's body/bodies.

Daniel Jomphe12:05:02

Got back to implementing a defn to wrap other functions with middleware and other augmentations. I'm surprised that the most common ways to call a function, with arity more than 1, bypass my middleware. Might be related to something I don't yet understand about intern or apply?

seancorfield15:05:27

Screenshots are not a good way to share code but I suspect you are only calling your wrapper with a single argument?

Daniel Jomphe15:05:02

(defn wrap-print [what-to-print]
  (fn wrapper [wrapped-fn]
    (fn with-print [& args]
      (print what-to-print)
      (apply wrapped-fn args))))

(defmacro wrap-fx
  [& body]
  `(let [s# (new java.io.StringWriter)]
     (binding [*out* s#]
       (let [r# ~@body]
         {:ret r#
          :out (str s#)}))))


(intern *ns* '+ ((wrap-print "Hello") clojure.core/+))
(wrap-fx (+ 1))   ; => {:ret 1, :out "Hello"}
(wrap-fx (+ 1 1)) ; => {:ret 2, :out ""}

Daniel Jomphe15:05:09

Towards more simplicity

(clojure.core/defn wrap-add [what-to-add]
  (fn wrapper [wrapped-fn]
    (fn with-add [& args]
      (+ what-to-add
         (apply wrapped-fn args)))))

(intern *ns* '+ ((wrap-add 100) clojure.core/+))
(+ 1)   ; 101
(+ 1 1) ; 2

seancorfield15:05:13

Try it with something other than a clojure.core function.

seancorfield15:05:54

Several core functions are optimized and inlined for different arities. Core is also AOT compiled and direct linked so overriding parts of core can be tricky -- it doesn't behave like user-space functions in some areas.

Daniel Jomphe15:05:24

Oh, looks like you got it... 🙂 will report back

seancorfield15:05:14

A workaround would be to use a wrapper around clojure.core/+ for this case:

user=> (defn plus [& args] (apply + args))
#'user/plus
(defn wrap-print [what-to-print]
  (fn wrapper [wrapped-fn]
    (fn with-print [& args]
      (print what-to-print)
      (apply wrapped-fn args))))
#'user/wrap-print
user=> (intern *ns* '+ ((wrap-print "Hello") plus))
#'user/+
user=> (+ 1)
Hello1
user=> (+ 1 1)
Hello2
user=> 

Daniel Jomphe15:05:16

haha I was trying this, but it gives a weird error:

(defn my-sub [& args] (apply - args))
; No value supplied for key: (apply - args)

seancorfield16:05:10

user=> (defn my-sub [& args] (apply - args))
#'user/my-sub
I'm guessing you've already overwritten - at least partially?

seancorfield16:05:26

Or maybe you overwrote defn?

Daniel Jomphe16:05:54

oh yes, that's right, I had overwritten defn and forgot to use core's one. Developing and testing in the same file makes things easier, until it conflates them due to these clashing specificities.

seancorfield16:05:30

Overriding core stuff is nearly always problematic 🙂

Daniel Jomphe16:05:02

Thanks - I'll rewrite my tests in terms of other code than core's. :)

Daniel Jomphe19:05:19

This did it, thanks again Sean!

respatialized20:04:53

Is there a version of clojure.repl/source that returns the actual source as data instead of a string?

dgb2320:04:03

You can read the string like so: (read-string (repl/source-fn 'frequencies))

dgb2321:04:09

even better:

(clojure.spec.alpha/conform
   :clojure.core.specs.alpha/defn-args
   (rest (read-string (clojure.repl/source-fn 'frequencies))))

🙂 2
Daniel Jomphe21:04:56

which yielded:

{:fn-name frequencies,
 :docstring "Returns a map from distinct items in coll to the number of times\n  they appear.",
 :meta {:added "1.2", :static true},
 :fn-tail
 [:arity-1
  {:params {:params [[:local-symbol coll]]},
   :body
   [:body [(persistent! (reduce (fn [counts x] (assoc! counts x (inc (get counts x 0)))) (transient {}) coll))]]}]}

😍 2
tstout23:04:52

Is there a way to disable commas in clojure.pprint for maps? My use case is generating build.clj and deps.edn scaffolding and then pprinting the result to files. The commas in maps make my eyes bleed. Other collections don't have commas when pprinted.

tstout00:04:27

I was about to go view the source. I am lazy and asked first. Currently I'm SOL. I can always transform to what I need before writing the files. I guess Rich had a preference for this.

seancorfield00:04:32

Commas are whitespace so they're purely cosmetic 🙂

devn00:04:48

perhaps zprint would be tunable to your specifications

😃 1