clojure

New to Clojure? Try the #beginners channel. Official docs: https://clojure.org/ Searchable message archives: https://clojurians-log.clojureverse.org/
cch1 2021-03-08T14:59:53.255Z

Here’s a code challenge that, if resolved, will help me test a library I am writing: how can one write an infinitely deep data structure in Clojure? Infinitely wide data structures are common. And it’s even not too difficult to infinitely generate a recursive data structure (but it never returns…). My goal is to find the analogue of the lazy seq but in depth. An example would be “a list of a list of a list of a list of ….“. A characteristic of this data structure is that I could iterate first on it forever.

Ben Sless 2021-03-08T15:02:38.255400Z

Won't something like

(defn lazy-tree
  []
  (lazy-seq
   (list
    (lazy-tree)
    (lazy-tree))))
satisfy this condition? It stack overflows, though

borkdude 2021-03-08T15:03:10.255600Z

I think any such data structure will stack overflow.

borkdude 2021-03-08T15:03:26.255800Z

(defn x [] [(x)]) (x)

Ben Sless 2021-03-08T15:04:03.256Z

Not if it's lazy, then maybe you can walk it

Ben Sless 2021-03-08T15:04:21.256200Z

seems like my lazy-tree doesn't stack overflow as long as you don't print anything

Ben Sless 2021-03-08T15:04:29.256400Z

(def f (first (lazy-tree)))
^ this is fine

borkdude 2021-03-08T15:05:32.256600Z

it's fine as long you don't do anything with f

borkdude 2021-03-08T15:05:38.256800Z

which is pretty useless ;)

cch1 2021-03-08T15:06:18.257200Z

You guys are fast. This was my first attempt, but it burns up tthe CPU…: ((fn f [] (lazy-seq (f))))

cch1 2021-03-08T15:07:28.257400Z

But attempting to get even the first element locks up. I’m not sure why since the very first element is not aggressively computed (you can perform class on the return of that fn call above).

Ben Sless 2021-03-08T15:09:56.257800Z

Let's ask the big question - why?

☝️ 1
cch1 2021-03-08T15:10:32.258100Z

I”m OK with blowing out the stack on printing or attempting to walk infinitely deep -that’s kinda the point of this structure. I want to be satisfied that my code lazily walks an infinitely deep data structure.

cch1 2021-03-08T15:12:42.258300Z

Just as first can force realization of the first of an infinitely wide data structure, I want to prove that my accessor ( a variant of a zipper) forces instantiation of only a finite number of recursions of an infinitely nested data structure.

cch1 2021-03-08T15:14:05.258500Z

@ben.sless ' attempt and mine have a common shape: a recursive call to a lazy seq with a single elements that is a call to a lazy…

Ben Sless 2021-03-08T15:14:30.258700Z

Actually, my example is both infinitely deep and infinitely wide

cch1 2021-03-08T15:14:57.258900Z

Yep. Mine is not, and just infinitely deep is OK.

Ben Sless 2021-03-08T15:14:58.259100Z

its width increases exponentially

cch1 2021-03-08T15:15:09.259300Z

I’ve already got the width test working.

Ben Sless 2021-03-08T15:15:43.259500Z

but what's the difference between an infinite lazy seq and an infinitely deep seq? an infinite lazy sequence is like a linked list. Isn't a linked list deep? or do you consider it wide?

cch1 2021-03-08T15:15:44.259700Z

But the infinintely deep part seems like it should work. But it clearly does not

cch1 2021-03-08T15:17:57.259900Z

I think the difference can best be explained with an example accessor: (iterate first data) should run infinitely long on an infinitely deep structure. It would not (necessarily) on an infinitely wide lazy seq.

cch1 2021-03-08T15:20:07.260400Z

Upon further reflection, here is my litmus test: (iterate first data) should neither error nor return only on an infinitely deep data structure while (iterate rest data) should neither errors nor return only on an infinitely wide data structure. (both modulo stack overflow or OOM conditions).

ghadi 2021-03-08T15:25:48.260800Z

still haven't answered why

ghadi 2021-03-08T15:26:16.261Z

at first reading this is classic https://xyproblem.info

cch1 2021-03-08T15:28:01.261300Z

WHy is easy: to test that a zipper-like constructor does not accidentally walk infinitely deep (and I know it does now, it’s not trivial to fix and it’s easy to regress -thus a unit test seems prudent).

cch1 2021-03-08T15:28:59.261500Z

…plus it seems like, for all it’s elegant handling of laziness, Clojure would have a tool for lazy recursive (in depth) data structures.

ghadi 2021-03-08T15:30:08.261900Z

several of the code snippets above will generate recursive datastructures

cch1 2021-03-08T15:30:31.262100Z

But none seem to be able to do so in finite time.

ghadi 2021-03-08T15:30:32.262300Z

whether or not you can print them is a separate concern

ghadi 2021-03-08T15:30:47.262500Z

I do not understand that assertion

cch1 2021-03-08T15:31:03.262700Z

Maybe I missed something…. checking my first solutiuon again…

cch1 2021-03-08T15:32:39.262900Z

Nope. Mine (for sure) and I think Ben’s solution (I think, more testing required) both cannot pass the test of (class (first data))

cch1 2021-03-08T15:33:09.263100Z

(class data) => LazySeq

cch1 2021-03-08T15:33:22.263300Z

(class (first data)) => ….

cch1 2021-03-08T15:35:20.263500Z

Looks like @ben.sless ' solution (`lazy-tree`) passes the “Access the first element lazily” test…

cch1 2021-03-08T15:37:05.263700Z

Here’s a slightly simplified version (only infinite vertically) of Ben’s lazy-tree that does indeed solve the problem:

cch1 2021-03-08T15:37:36.263900Z

(def lazy-tree
  ((fn f [] (lazy-seq
            (list
             (f))))))

cch1 2021-03-08T15:37:45.264100Z

Thanks, Ben.

cch1 2021-03-08T15:39:07.264300Z

Correctly blows out the stack on (iterate first (lazy-tree)).

2021-03-08T15:42:08.264900Z

Can we auth with maven directly in deps.edn? https://clojure.org/reference/deps_and_cli#_maven_authenticated_repos

2021-03-08T15:42:46.265300Z

Basically I want the setting go with the project, instead of residing in $HOME

Endre Bakken Stovner 2021-03-08T16:45:14.271600Z

I have the following function which recursively reads all forms in a file:

(import '[<http://java.io|java.io> PushbackReader])
(require '[<http://clojure.java.io|clojure.java.io> :as io])
(defn read-all
  [file]
  (let [rdr (-&gt; file io/file io/reader PushbackReader.)]
    (loop [forms []]
      (let [form (try (read rdr) (catch Exception e nil))]
        (if form
          (recur (conj forms form))
          forms)))))
It works well, however, I want to read maps within the forms as sorted-map. One inelegant way I can do that is to switch all instances of {:bla "bla"} in file to (sorted-map :bla "bla") before sending it to read-all.
(spit "test.json" (into (sorted-map) (sort (zipmap (range 50 0 -1) (repeat 1)))))
(read-all "test.json") ;; [{7 1, 20 1, 27 1, ...}] ;; Darn!
Can you think of a better way to get the items in hash-maps in the order they are given? Is it possible with clojure.edn/read and using the :readers opt?

ghadi 2021-03-08T16:49:03.272600Z

maps have no order

ghadi 2021-03-08T16:50:23.273200Z

if you want to post-process them into a sorted-map, that is possible

🙏 1
ghadi 2021-03-08T16:50:41.273900Z

if you know that the keys are sortable ahead of time

Endre Bakken Stovner 2021-03-08T16:58:00.276600Z

I'd love to be able to read {4 1, 2 3, 6 7} from a file into an array-map so the order given is preserved.

borkdude 2021-03-08T16:58:58.277Z

@endrebak85 The array map threshold is an implementation detail. Just store a seq of tuples.

borkdude 2021-03-08T16:59:21.277300Z

Or use e.g. clj-commons/ordered

ghadi 2021-03-08T17:01:19.279100Z

what are you really trying to do by preserving written order?

Endre Bakken Stovner 2021-03-08T17:20:24.286Z

I am creating a little DSL where {} preserves the order written. So when I read a snippet of a file written in my DSL stuff enclosed in braces becomes a seq of tuples (as per @borkdudes suggestion).

(def example-dsl-snippet "{1 1 2 1 3 1 4 1 5 1 6 1 7 1 8 1 9 1 10 1 11 1 12 1}") 
(read-string example-dsl-snippet) ;; this loses order: {7 1, 1 1, 4 1, 6 1, 3 1, 12 1, 2 1, 11 1, 9 1, 5 1, 10 1, 8 1}
I'd love a function that could read a string describing a map so it becomes (seq '([1 1] [2 1] [3 1] ...)), not a map.

borkdude 2021-03-08T17:21:07.286400Z

@endrebak85 Is that DSL used from Clojure programs? Then that may be very confusing

2021-03-08T17:21:46.287100Z

yeah, you just can't assume order for maps like that, so you can't use maps for a dsl like that

👍 1
borkdude 2021-03-08T17:22:54.287700Z

you could also store the order within the map {:a 1 :b 2 :order [:a :b]}

Endre Bakken Stovner 2021-03-08T17:22:59.287800Z

Yes, but the DSL is basically json. It is not meant for Clojure programmers, but people with experience with a Python DSL called https://snakemake.readthedocs.io/. For those users, this would be expected behavior.

borkdude 2021-03-08T17:23:36.288Z

But you want to parse this using a Clojure parser?

Endre Bakken Stovner 2021-03-08T17:25:07.288200Z

But this would require my users to add the order above. I guess preprocessing the file to switch {-1 5 0 3} into (seq '([-1 5] [0 3])) is the best way to do it. Or just use a vec of [kw val] tuples.

Endre Bakken Stovner 2021-03-08T17:25:48.288400Z

Yes. I guess I should just use a vector of tuples: [[kw val] [kw2 val2] ...] instead

borkdude 2021-03-08T17:26:35.289100Z

If this language is only used from say Python, but you want to parse / use it from Clojure (for whatever reason), I would just roll my own parser for this and not try to parse this as EDN

🙏 1
Endre Bakken Stovner 2021-03-08T17:26:37.289200Z

Thanks for the suggestions and help btw.

Endre Bakken Stovner 2021-03-08T17:31:39.290700Z

This is what I thought too, but an answer with 11 upvotes on Stack Overflow suggested using array-map so I thought perhaps I was mistaken.

borkdude 2021-03-08T17:32:14.291200Z

Afaik array-map is just an optimization detail

quadron 2021-03-08T17:32:58.292Z

why do we have [vswap! swap! alter send] for [volatile atom ref agent]?

quadron 2021-03-08T17:34:13.293Z

is there a reason for nonexistence of a polymorphic name for different ref-like-values?

quadron 2021-03-08T17:34:44.293400Z

or perhaps i am not aware of that protocol?

borkdude 2021-03-08T17:35:41.294300Z

There is no protocol for atoms in Clojure, only interfaces (IAtom and IAtom2)

1
Endre Bakken Stovner 2021-03-08T17:37:28.294400Z

Yeah, 8 seems to be the magic cutoff on my machine:

user=&gt; (into (array-map) (into (sorted-map) (zipmap (range 9 0 -1) (repeat 1))))
{7 1, 1 1, 4 1, 6 1, 3 1, 2 1, 9 1, 5 1, 8 1}
user=&gt; (into (array-map) (into (sorted-map) (zipmap (range 8 0 -1) (repeat 1))))
{1 1, 2 1, 3 1, 4 1, 5 1, 6 1, 7 1, 8 1}

seancorfield 2021-03-08T17:41:42.296800Z

Q about maintaining backward compatibility: if I have some protocol that declares three functions and may have additional user-supplied implementations out in the wild, and I add a fourth function to that protocol (and implement it in my code), could that break users' existing code?

seancorfield 2021-03-08T17:43:07.298200Z

My immediate reaction is that "changing a protocol" is breaking, since the protocol is part of the public API -- but if folks only provide a partial implementation and never call the unimplemented functions anyway, it wouldn't affect them?

ghadi 2021-03-08T17:44:04.299800Z

"changing a protocol" is breaking -- this isn't quite precise does adding the new method break existing users? -> no, so it's not breaking

ghadi 2021-03-08T17:44:32.300500Z

unless there is something about the way the unimplemented method is used -- it's tricky stuff

seancorfield 2021-03-08T17:44:39.300800Z

(I'm agonizing over whether to extend next.jdbc/execute-batch! the same way execute! et al operate so you can pass a connectable or sourceable instead of just a PreparedStatement)

ghadi 2021-03-08T17:44:55.301400Z

ostensibly something is calling the unimplemented method

Ed 2021-03-08T17:48:56.303100Z

because they have different semantics? ... refs need to be used in a transaction. Agents are asynchronous. Volatile's and Atoms have different thread guarantees. I'm not sure that anyone should be pretending that they're the same to write to ... only that you can deref them to read

quadron 2021-03-08T17:52:44.304200Z

it's not about pretending they are the same

quadron 2021-03-08T17:53:22.304400Z

it's about having an abstract operator with a unique name that has different semantics based on the value it is called with

Ed 2021-03-08T18:01:41.306500Z

Abstractions are about pretending a group of things are the same in a specific way. Clojure's reference types are the same in that they can always be read without a significant cost (unlike a remote actor which would require a network request, for example) because they're in ram and (ideally) wrap a persistent data structure. But they all have different semantics for writing to, so what's the point of an abstract function? Isn't that just a common name that obscures the important difference between why you're using one reference type over another?

Ed 2021-03-08T18:02:13.306700Z

I'm not sure I understand why that would be a positive thing

quadron 2021-03-08T18:11:04.307Z

obscures it for whom? why would an expression have to locally demonstrate the semantics of it's execution if the semantics doesn't matter locally?

quadron 2021-03-08T18:11:36.307200Z

at the local point where swap!/vswap!/... is called, the knowledge about the type of the ref does not matter

quadron 2021-03-08T18:12:26.307400Z

after all, is the ref itself not telling you that difference?

quadron 2021-03-08T18:13:06.307700Z

(circle-area circle) (square-area square)

aaron-santos 2021-03-08T18:16:14.308300Z

Might also depend on how much weight one puts behind Hyrum's Law

aaron-santos 2021-03-08T18:16:52.308400Z

https://www.hyrumslaw.com/

1
alexmiller 2021-03-08T18:21:23.309Z

is there an actual api doc anywhere for next.jdbc? not seeing it on the readme

alexmiller 2021-03-08T18:21:40.309200Z

ah, nvm I found it

alexmiller 2021-03-08T18:25:32.310100Z

so you're talking about adding -execute-batch to Executable and calling it from execute-batch! ?

alexmiller 2021-03-08T18:26:38.311100Z

are there known implementors of Executable outside next.jdbc?

vemv 2021-03-08T18:27:20.311300Z

Let's say there was a a protocol for writes of ref-like things. Then the underlying interface would be "megamorphic" which makes function inlining less likely Not sure if this is the precise reason though, but megamorphic site:<http://clojure.org|clojure.org> returns some hits

Ed 2021-03-08T18:28:03.311700Z

> why would an expression have to locally demonstrate the semantics of it's execution if the semantics doesn't matter locally? Because it matters in the execution of the program. If it's a agent, then I can't deref it and expect it to have the value I've just sent it, or if it's a ref I have to have a transaction open make a change. > at the local point where swap!/vswap!/... is called, the knowledge about the type of the ref does not matter I'm not sure I can agree with that. I think at that point it very much matters. My send to an agent has completed, what can I do with that agent, vs the equivalent on an atom. > after all, is the ref itself not telling you that difference? Yes, the constructors are different. But so are the update semantics, and also the reasons that an update might fail or be retried. These differences are what would be obscured by an abstraction that allowed you to easily exchange one for another. It would also need to take into account things like transactions, asynchrony, livelocks and deadlocks (for example), and complicate the ease of use of things like atoms. Also, how would it deal with alter vs commute? Would it need to support both update strategies on all reference types?

alexmiller 2021-03-08T18:28:18.311900Z

a quick skim through http://grep.app leads me to believe the answer is no

Ed 2021-03-08T18:29:10.312700Z

I think history is also a reason that this doesn't exist. The STM predates protocols in Clojure.

seancorfield 2021-03-08T18:31:32.315Z

Yeah, that's the specific case here but I was also sort of asking in the more general case about whether expanding the list of functions in a protocol is accretive or breaking, and under which circumstances.

seancorfield 2021-03-08T18:32:10.315900Z

(and thanks for checking http://grep.app for implementations -- I hadn't gotten far enough down the path to do that work)

alexmiller 2021-03-08T18:32:21.316200Z

I would have to think harder about whether this is a breaking change in the face of compilation and all the scenarios there but from a source pov, I think you are only going to call the new method if someone passes you a connectable/sourceable to execute-batch! (and those weren't possible before other than PreparedStatement, which presumably you will fix up)

seancorfield 2021-03-08T18:33:38.317200Z

Yes, the intention is to try to do this in a purely accretive way -- so new uses become possible, without breaking any existing uses.

seancorfield 2021-03-08T18:34:42.317300Z

I guess I should add a more obvious link to the API docs then...

alexmiller 2021-03-08T18:35:12.317900Z

in general, adding a method to an interface in Java is NOT considered a change that breaks binary compatibility

quadron 2021-03-08T18:35:20.318200Z

your take would have made sense to me if one of swap!/vswap!/alter!/send had implementations for more than one ref-type

alexmiller 2021-03-08T18:35:32.318600Z

(mentioning b/c protocols have a backing Java interface ofc)

alexmiller 2021-03-08T18:38:23.319500Z

given that protocols already handle the case of missing protocol methods, I think from a Clojure POV, it would just be seen in the same way

Ed 2021-03-08T18:39:25.320Z

I'm not sure I understand why that makes more sense ... can you explain further, please?

seancorfield 2021-03-08T18:39:43.320700Z

(done; also link to older docs under old group ID)

alexmiller 2021-03-08T18:40:09.321200Z

so I want to say - this is not a breaking change in that sense

alexmiller 2021-03-08T18:40:16.321500Z

thx

seancorfield 2021-03-08T18:40:20.321700Z

Thanks, Alex. That's good feedback and justification, and makes me feel more comfortable about it.

p-himik 2021-03-08T18:40:49.322100Z

Alternatively, you can explicitly use array maps. The order will be preserved. Do note however, that assoc, dissoc, and any other function that returns a new map may change its type to a hash map.

alexmiller 2021-03-08T18:41:17.322800Z

it's hard for me to see any case in what you describe where something that used to work now does not work. merely that new additional things may need to be added to make external extensions work with the new api

alexmiller 2021-03-08T18:41:37.323100Z

and I don't even see any cases of that in the wild

1
quadron 2021-03-08T18:49:15.323400Z

all these functions are always called in the presence of the ref-like value, so there is no confusion there with respect to alter/commute issue, in the absence of knowledge, it would call whichever operation that is more generic (expects less structure from data)

kah0ona 2021-03-08T18:54:49.326800Z

Hello folks, just a quick question. I have a ring handler that does some long I/O-operation. I want it to start the operation, and then to return immediately with a URL, which a client can poll for to see progress. The progress is stored in an atom. Now I simply wrap the long operation in a future, but is that idiomatic? If I never deref the future, would that be a problem, ie. would that mean that eventhough the future goes out of scope, it would still linger/run somewhere?

2021-03-08T18:55:55.327Z

yes

2021-03-08T18:56:22.327600Z

a future is another thread, it has a life of its own once started regardless of being deref'ed or not

kah0ona 2021-03-08T18:56:49.328100Z

ok, so if that doseq that’s in there finishes, the thread will be destroyed?

2021-03-08T18:57:04.328300Z

sure

kah0ona 2021-03-08T18:57:24.329Z

ok thanks

2021-03-08T18:57:35.329500Z

(a future actually runs in a threadpool, so in theory instead of being destroyed the thread may run another future)

kah0ona 2021-03-08T18:57:45.329800Z

what if like 8 requests or so come in at roughly the same time

2021-03-08T18:58:02.330500Z

multithreading man

2021-03-08T18:58:05.330800Z

its a thing

kah0ona 2021-03-08T18:58:21.331200Z

I know haha 🙂 ok

kah0ona 2021-03-08T18:58:39.331600Z

i was just wondering if it could fill up the threadpool

2021-03-08T18:59:53.332900Z

in general it is not good practice to just fire off futures willy nilly, because usually you will end up wanting more control over their execution, but if you don't want more control then there is nothing wrong with it

kah0ona 2021-03-08T19:00:09.333100Z

thanks ok

kah0ona 2021-03-08T19:01:29.333700Z

I’ll use the well-let’s-refactor-if-it-goes-boom approach then

kah0ona 2021-03-08T19:02:20.334600Z

(ie. my definition of pragmatic ;-))

borkdude 2021-03-08T19:02:52.335100Z

@kah0ona alternatively you could put the work on a queue and have a worker that constantly consumes work from that queue

borkdude 2021-03-08T19:03:16.335500Z

but I've used the "`future` and forget" approach myself a lot as well ;)

kah0ona 2021-03-08T19:08:07.337100Z

yeah, that’ll be the approach i’ll take when everything has screeched to a halt, if that ever happens 🙂 but it’s a quite infrequently used endpoint, so i’m winging it

Ed 2021-03-08T19:13:19.337300Z

> all these functions are always called in the presence of the ref-like value, so there is no confusion there I think that's what I'm getting at. There is confusion. Everything about calling one of these update functions is different, apart from the fact that they all do some form of state management > with respect to alter/commute issue, in the absence of knowledge, it would call whichever operation that is more generic (expects less structure from data) alter updates a ref in a compare and swap kind of way (but consistently within the transaction), wheras commute allows for commutable operations to be reordered if there's a transactional conflict. This isn't supported by any the other reference types and is an example of where the semantics up updating a ref differ from an atom, for example.

emccue 2021-03-08T20:27:20.337800Z

it can definitely fill up the threadpool - that is a thing

emccue 2021-03-08T20:27:37.338200Z

but the abstraction is still what you want

phronmophobic 2021-03-08T20:30:10.339100Z

there's also the https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html interface from the JVM with a few options that provide more control

alexmiller 2021-03-08T20:44:46.339600Z

you cannot "fill up" the future threadpool, it will grow without limit

👍 1
alexmiller 2021-03-08T20:45:02.339800Z

(and shrink automatically)