Fork me on GitHub
#clojure
<
2022-10-05
>
Drew Verlee02:10:43

what does it mean to be in the clojure namesapce? I notice that clojure.spec.alpha is included in a new clojure project (1.11.?) but clojure.tools.logging isn't.

hiredman02:10:08

Neither is in the clojure namespace

hiredman02:10:33

They have namespaces where the name starts with clojure

hiredman02:10:08

Namespaces are flat

hiredman02:10:41

The main thing is usually if a namespace name starts with clojure that means it belongs in some way to rhickey

hiredman02:10:35

There is no enforcement, but rich has asserted that clojure.* belongs to him and we should attempt no landings there

monolith 1
Drew Verlee02:10:42

roger that makes sense.

hiredman02:10:50

At one point there was a stronger distinction to be made between the set of namespaces that come with the clojure language, and "contrib" as a kind of central set of extra libraries

hiredman02:10:14

The distinction is still kind of there, but spec has started to blur the line, in that it is a separate artifact from clojure, but clojure depends on it

👍 1
👀 1
seancorfield04:10:25

@U0DJ4T5U1 See https://clojure.org/dev/contrib_libs for more information and a list of Contrib libraries. Technically clojure.spec.alpha and clojure.core.specs are separate libraries (but still considered "core") and Clojure itself depends directly on those which is why you see this:

seanc@Sean-win-11-laptop:~/clojure/empty$ clojure -Stree
org.clojure/clojure 1.11.1
  . org.clojure/spec.alpha 0.3.218
  . org.clojure/core.specs.alpha 0.2.62
-- they are literally transitive dependencies of org.clojure/clojure. As someone who maintains five of the Contrib libs, I'm happy to answer any more Qs you have 🙂

Drew Verlee04:10:26

I was mostly trying to understand how to find things. I know i can just require clojure.spec.alpha. So i tried the same with clojure.tools.logging and discovered i couldn't.

Drew Verlee04:10:26

in my case, the answser is that i can use emacs to search for namespaces that are available. or at least i think i can...

Drew Verlee04:10:00

or i'm sure there are a couple ways that all result in roughly the same information.

Drew Verlee04:10:06

Thanks for the offer! Don't have any questions at the moment.

seancorfield04:10:27

Yeah, figuring out what dependencies are needed is not always intuitive, given that namespaces and group/artifact IDs have no connection to each other and may not be at all related in some projects...

seancorfield04:10:56

All these namespaces are in Clojure itself: https://clojure.github.io/clojure/ and if you drill into https://clojure.github.io/clojure/clojure.core-api.html you'll see three namespaces "attached" to clojure.core: protocols, reducers, and server (which further reinforces the incorrect mental model of them somehow being "nested"!).

Drew Verlee04:10:18

> clojure.core.reducers >

A library for reduction and parallel folding. Alpha and subject
> to change.
>

Drew Verlee04:10:05

sorry, i keep hitting enter to soon, because i have had to mess with my keyboard. Anyway, yeah i see how it's attached.

seancorfield04:10:40

Yeah, it's just an artifact of the documentation generator really that they're listed "nested" like that -- but it seems common across several Clojure doc gen tools 😐

Drew Verlee04:10:41

gotcha. yeah, i mean, i nest my files according to the namespace so its hard to break that abstraction.

seancorfield04:10:34

Heh, and that convention which Clojure follows reinforces it further 🙂

Ben Sless10:10:31

You can always look for stuff on clojars or use neil

Drew Verlee04:10:32

Are there any good example of using https://github.com/clojure/tools.logging on a clojure deps built project? Would such a thing be useful for others sense i'm going to end up having to produce some for myself to understand it better? I noticed there were a couple code examples in the https://cljdoc.org/d/org.clojure/tools.logging/0.4.1/doc/readme and now there gone. But not really gone because google is bumping that example link up because people are using it. Which is going to be weird if they use the old version.

seancorfield04:10:59

Logging on the JVM is a bit of a mess and people have different opinions on which JVM logging library to use (with clojure.tools.logging). LambdaIsland had a gone article about setting this up -- but at work we chose log4j2 rather than what they picked (but the setup is very similar).

seancorfield04:10:29

Not sure what "clojure deps built project" has to do with it -- whether you use Leiningen or the CLI has pretty much zero impact on how c.t.l is used... except perhaps around the JVM property for selecting the logging implementation if you want something that isn't the default.

Drew Verlee04:10:48

> Not sure what "clojure deps built project" has to do with it -- whether you use Leiningen or the CLI has pretty much zero impact on how c.t.l is used... except perhaps around the JVM property for selecting the logging implementation if you want something that isn't the default. That's good to know for sure. I'm building up a toy project as i tear down the one where logging isnt working, hopefully ill meet it in the middle and figure out the problem.

Drew Verlee04:10:13

ok cool. I'll dive into that again with anger this time.

seancorfield04:10:35

All of our projects at work have this:

;; use log4j 2.x:
        org.apache.logging.log4j/log4j-api {:mvn/version "2.19.0"}
        ;; bridge into log4j:
        org.apache.logging.log4j/log4j-1.2-api {:mvn/version "2.19.0"}
        org.apache.logging.log4j/log4j-jcl {:mvn/version "2.19.0"}
        org.apache.logging.log4j/log4j-jul {:mvn/version "2.19.0"}
        org.apache.logging.log4j/log4j-slf4j-impl {:mvn/version "2.19.0"}
and then all of our processes are started with:
-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory
to ensure we get log4j2 instead of one of the others on the classpath!

Drew Verlee04:10:06

:heart_on_fire:

Drew Verlee04:10:41

were using

ch.qos.logback/logback-classic {:mvn/version "1.1.7"}
org.clojure/tools.logging {:mvn/version "1.2.4"}

Drew Verlee04:10:00

i should probably look at logback-classic docs.

seancorfield04:10:43

You probably want to add the various bridging libraries explicitly to ensure "all" logging for all your dependencies is routed through logback and so you can control all of it centrally via logback config.

Drew Verlee04:10:17

reading this right now https://www.marcobehler.com/guides/java-logging and https://lambdaisland.com/blog/2020-06-12-logging-in-clojure-making-sense-of-the-mess Because i forgot to sign up for CS 400 Java logging and languish laughter

Drew Verlee04:10:45

I'm i 80% correct in assuming That clojure/java project needs to gather the output from all the logging frameworks in my project (including nested deps) into the logging framework it wants? are libraries that do that "bridging libraries"?

seancorfield04:10:35

Yes, if you want to have control over all of your app's logging -- even that coming from dependencies -- then you need those libraries that route logging from the various log libraries into the one you want to use/control.

seancorfield04:10:18

It's really unfortunate that Java has ended up with so many and they're all slightly incompatible in subtle ways. But at least the bridging libs exist to paper over it.

Drew Verlee05:10:14

I want to say something philosophical about how time and tide makes us sinners and saints all.

seancorfield05:10:28

Hahaha... At least with logging, once you have a configuration that "works" you can mostly forget about it. Modulo security issues forcing you to update the whole stack.

Drew Verlee05:10:46

One troubles me is things like this > The problem with JCL is, that it relies on https://articles.qos.ch/thinkAgain.html? to find out which logging implementation it should use - at runtime. And that can lead to a lot of pain. In addition, you’ll find that the API is somewhat inflexible, that it comes with a fair amount of cruft, and there’s simply better alternatives out there, nowadays. from https://www.marcobehler.com/guides/java-logging Now, i'm i going to click that link and read the argument on why JCL was a "hack" maybe the author was horrible horrible wrong and JCL was the brilliant! What's the "professional" thing to do? The answer is to write your own logging framework and not publish it anywhere.

seancorfield05:10:42

It took us a while to get it working to our satisfaction and, for the most part, any one given library is as good as the others for casual use -- and bridging libraries exist for all of them to all of the others I think. But we like log4j2 because a) you can configure it with .properties files instead of XML b) you can control the configuration at startup with env vars and/or JVM properties and c) you can have it auto-reload the config when it changes at runtime (useful for long-lived processes). But with log4j2 not being the first in the list c.t.l searches for, it is a bit of a pain to have to specify the JVM property for all processes so override c.t.l's default behavior 😞

👀 1
seancorfield05:10:58

Given the order that c.t.l looks for implementations https://github.com/clojure/tools.logging/blob/master/src/main/clojure/clojure/tools/logging/impl.clj#L249-L253 it's definitely "easier" to go with slf4j + logback per Lambda Island's article...

Drew Verlee05:10:00

Yeah, im getting a java perspective a bit then im going to read that one.

seancorfield05:10:37

Probably my biggest pet peeve with sl4j is that it treats error and fatal as the same error level 😡 :

seanc@Sean-win-11-laptop:~/clojure$ cat src/example.clj
(ns example
  (:require [clojure.tools.logging :as log]))

(defn -main [& args]
  (log/info "Hello, world!")
  (log/warn "Hello, world!")
  (log/error "Hello, world!")
  (log/fatal "Hello, world!"))
seanc@Sean-win-11-laptop:~/clojure$ cat deps.edn
{:deps {org.clojure/tools.logging {:mvn/version "1.2.4"}
        org.slf4j/slf4j-api {:mvn/version "1.7.30"}
        org.slf4j/jul-to-slf4j {:mvn/version "1.7.30"}
        org.slf4j/jcl-over-slf4j {:mvn/version "1.7.30"}
        org.slf4j/log4j-over-slf4j {:mvn/version "1.7.30"}
        org.slf4j/osgi-over-slf4j {:mvn/version "1.7.30"}
        ch.qos.logback/logback-classic {:mvn/version "1.2.3"}}}
seanc@Sean-win-11-laptop:~/clojure$ clojure -M -m example
22:14:02.022 [main] INFO example - Hello, world!
22:14:02.023 [main] WARN example - Hello, world!
22:14:02.024 [main] ERROR example - Hello, world!
22:14:02.024 [main] ERROR example - Hello, world!
Grrr!!!! With other logging libraries, that last line would be FATAL instead.

loading 1
Drew Verlee06:10:31

Is "CONSOLE" a valid logback.xml appender-ref? I don't see it listed in the https://logback.qos.ch/manual/appenders.html.

<root level="DEBUG">
    <appender-ref ref="CONSOLE" />
  </root>
Maybe... maybe our project made an appended-ref named console?

Drew Verlee06:10:24

I don't care that its xml, what troubles me is that i cant find what values it can support. It's like typed but not in a way that's useful.

Drew Verlee07:10:42

well it seems to work* so it's almost certinally an ok value from somewhere. i'm still not sure

msolli07:10:33

Here’s my minimal logback.xml:

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT"/>
  </root>
</configuration>
It could be that CONSOLEexists as a default appender, like you hypothesize. Maybe better to make it explicit by defining it, though?

Drew Verlee07:10:55

@U06BEJGKD by "defining it" do you mean write an implementation for console? Or change CONSOLE to STDOUT?

msolli07:10:22

The latter - include a <appender>section with a name that you then reference in the root logger. In my example: name="STDOUT"is referenced in <appender-ref ref="STDOUT"/>.

Drew Verlee07:10:53

ah thanks! Yeah ok, so our configuration does have that.

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <charset>UTF-8</charset>
      <pattern>%msg%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>DEBUG</level>
    </filter>
  </appender>

Drew Verlee07:10:42

xml, it's like a programming language only everyone defines what it means.

msolli08:10:58

Yeah, that’s what the X is. 🙂

1
lread12:10:49

I come from a Java background so am used to all the logging options and config. But when I started contributing to cljdoc noticed it uses https://github.com/pyr/unilog. Maybe that would be of help for those who want to avoid some of the complexity?

👀 1
niquola09:10:44

Hi guys, I'm looking for a library to stop, persist and restore Clojure code at specific points for workflow engine. Something like core.async (or continuation passing style) but with more control and customization and it's ok with some additional restrictions!

delaguardo11:10:38

I did quite extensive research about a year ago and found nothing. As a result my colleague made a stack based programming language that sits on top of clojure 🤷 - https://github.com/DeLaGuardo/jjoy/blob/main/src/jjoy/core.cljc#L188

👍 1
niquola09:10:38

(if (= :ok (async-fn ...))
   (let [res-1 (async-fn-2 ...) res-2 (async-fn-2' ...)]
        (async-fn-2'' res-1 res-2))
   (async-fn-3 ...))

jpmonettas12:10:07

Hi everybody! Does anybody knows a way of creating objects ids for JVM objects,so remote systems can refer to them by id? I have tried System/identityHashCode but have found collisions pretty easily. Also I don't want to use uuids because then remote clients can't cache results. Also tried to keep a map of obj->incremental_id but not all objects are hashable, like (def o (range))

reefersleep12:10:47

You want a remote system to have an id to refer to an (currently!) in-memory, transient object?

jpmonettas12:10:35

yes, is for a dev tool. I'm retaining the pointers so the GC don't collect them and a remote system (external GUI) should be able to "ask questions" about them by id

p-himik12:10:38

> Also I don't want to use uuids because then remote clients can't cache results. How does usage of UUIDs prevent caching exactly?

p-himik12:10:22

Thought it sounded familiar and then found your older question about CLJS. :D https://clojurians.slack.com/archives/C03S1L9DN/p1663071702855139

jpmonettas12:10:17

haha yeas, I have solved it for CLJS by monkey patching the objects

p-himik13:10:17

Mmm. What prevented you from using an external ID registry?

jpmonettas13:10:15

> How does usage of UUIDs prevent caching exactly? so one way of doing it is everytime the client calls (give-me-list-of-objects-a) you can create a uuid for each of them, update a map off uuid->obj and return the list of uuids. Then you do the same for (give-me-list-of-objects-b). The thing is those will be different uuids even if the objects are the same. So if the client is calling a cachable (print-object uuid), it will miss the cache. Does it make sense?

jpmonettas13:10:32

> Mmm. What prevented you from using an external ID registry? wdym. The problem with a registry is that you need a map from {obj -> id} and you can't hash infinite sequence objects for example

p-himik13:10:04

1. A client sends a message to the server with (give-me-list-of-objects-a) 2. The server calls that function and serializes its result in a way that: a. Checks the internal registry of UUIDs b. Generates UUIDs for objects that haven't been encountered yet c. Reuses already generated and stored UUIDs for all other objects d. Includes UUIDs of the serialized objects in the serialized result 3. The server sends the serialized result+UUIDs to the client 4. The client deserializes the data and updates its own bidirectional object<->UUID registry 5. Whenever the client needs a particular object, it just asks the server by UUID

p-himik13:10:54

That's exactly how e.g. https://bokeh.org/ works, which I used to maintain. Well, almost - the IDs there are stored in the objects themselves because only the Bokeh-specific data is referenced and because Python and JS are mutable. But in principle using a registry that's separate from the data itself is not that different.

jpmonettas13:10:57

> 2. b Generates UUIDs for objects that haven't been encountered yet how do you know a object hasn't been encountered yet?

p-himik13:10:15

You store all the objects in the bidirectional object<->UUID registry.

p-himik13:10:49

And, as I've mentioned in that older thread, with this approach there's no need for UUIDs because you can just generate IDs as ever increasing integers.

jpmonettas13:10:02

how can you create such a map when you have unhashable objects like infinite lazy sequences?

jpmonettas13:10:03

the problem when I tried that solution was how to handle objects like (def o (range))

p-himik13:10:16

Check in some way whether the collection is potentially infinite (there's been some discussions) and store them in the registry by their System/identityHashCode. Of course, internally the object<->ID registry is not a simple map now but rather something like:

{:hashable   object<->ID
 :unhashable object-identity-hash-code<->ID}

p-himik13:10:44

The object-identity-hash-code<->ID is a crooked bidirectional map that's comprised of object-identity-hash-code->ID and ID->object.

jpmonettas13:10:37

yeah, but you end up again using identityHashCode which have collisions

jpmonettas13:10:52

I mean, I guess using it less, you reduce the risk

p-himik13:10:35

Perhaps. But in which cases does it have collisions?

p-himik13:10:23

I'm trying to imagine how it all works in my head. And I think I understand why an ID->object mapping is needed - after all, a client needs to be able to ask the server about some particular object by mentioning its ID. But when do you need the object->ID mapping?

Joshua Suskalo13:10:32

When you are listing objects not by id and need to ensure that the same object has the same id

jpmonettas13:10:36

sorry, got a phone call

jpmonettas13:10:52

> Check in some way whether the collection is potentially infinite the thing is that a lot of objects are lazy seqs and you can't know if it is infinite or not

jpmonettas13:10:40

> But in which cases does it have collisions? when I'm running on millions of objects I find collisions pretty quickly when using identityHashCode

jpmonettas13:10:06

> But when do you need the object->ID mapping? this is for returning the objects, you need to know for a object that you are about to return if you already have an assigned id, so you always return the same id for the same object for allowing caching on the client

jpmonettas13:10:26

damn System/identityHashCode, don't know why it is called identity is so easy to find twins objects :

(defn find-twins []
  (let [hs (java.util.HashSet.)]
    (loop [i 0]
      (let [o (Object.)
            o-hash (System/identityHashCode o)]
        (if (.contains hs o-hash)
          (println "Found after " i)
          (do
            (.add hs o-hash)
            (recur (inc i))))))))

dev> (find-twins)
Found after  71390

dev> (find-twins)
Found after  108724

dev> (find-twins)
Found after  21259

p-himik13:10:14

> the thing is that a lot of objects are lazy seqs and you can't know if it is infinite or not You can't know, yes - that's why I said "potentially". :) There were discussions here on how to detect something like this. A seq, realized?, not a range, maybe something else. > when I'm running on millions of objects I find collisions pretty quickly when using identityHashCode And are you sure that previously generated objects haven't been GC'ed?

p-himik13:10:35

Ah, you example seems to confirm that, hmm.

p-himik13:10:14

Oh, wait, I read it wrong. The old instance of object should be GCed in your case.

jpmonettas13:10:41

I tweaked find twins to be sure they are not GCed but its the same

(defn find-twins []
  (let [hs (java.util.HashSet.)
        ohs (java.util.HashSet.)]
    (loop [i 0]
      (let [o (Object.)
            o-hash (System/identityHashCode o)]
        (if (.contains hs o-hash)
          (println "Found after " i)
          (do
            (.add hs o-hash)
            (.add ohs o)
            (recur (inc i))))))))

Joshua Suskalo13:10:54

also identity hash code can't not generate duplicates. The "identity" part of "identity hash code" means that it's a hash code generated from the identity of the object, which means the first time the hash is generated it uses the object's location in memory and then cached. It's identical to the default implementation of Object#hashCode. The entire point is that it generates few duplicates, but for any sufficiently large number of objects it will generate duplicates.

👍 1
jpmonettas13:10:00

dev> (find-twins)
Found after  112484

dev> (find-twins)
Found after  44218

dev> (find-twins)
Found after  33766

Joshua Suskalo13:10:44

The reason to use identityHashCode is if the hashCode of the object you're using can't be guaranteed to have good properties for hashing, like infinite seqs which by clojure's spec must hash to the same thing as a collection with the same elements.

👍 1
jpmonettas14:10:06

> but for any sufficiently large number of objects it will generate duplicates. like 33k :face_with_rolling_eyes:

jpmonettas14:10:10

so I guess the only strategy would be for the JVM to add an incremental id on the instance header, but is for sure a waste of memory

Joshua Suskalo14:10:14

Yeah. You can't expect it to not generate duplicates. The only true way to see if two things are different objects is to stop the world with the GC and compare references, but since references aren't stable over time because of the GC it must be done via Object#equals if you want a stable idea of equality over time (which also requires the objects be immutable to check that really), so the only other way to do that is to introduce this mapping from id->object, but you can never store it the other way around if you don't want to use equality checks to validate things are the same.

p-himik14:10:17

Interesting. But still, you can use it. After all, hash maps rely on hashes that result in collisions from time to time. > When you are listing objects not by id and need to ensure that the same object has the same id Not sure I understand this part though @U5NCUG8NR. Can you give a concrete user-facing example?

Joshua Suskalo14:10:44

@U0739PUFQ the JVM already stores what amounts to a counter on the object header, and lots of other things. The Hotspot object header is something like 16 bytes already. Adding one more counter is substantial, but it's not like it isn't already happening.

Joshua Suskalo14:10:02

That "counter" being the 32 bit identity hash code of the object, plus other things that are in the header.

jpmonettas14:10:12

@U2FRKM4TW you need a two way mapping for this to work obj-id <-> obj, the obj->obj-id one is the problematic for infinite sequences

Joshua Suskalo14:10:18

@U2FRKM4TW stated earlier in the thread was the need for a function like (list-objects-of-type-a) since this is for a debugging tool. This requires you to just list out all objects, which requires you to have a mapping for ids for them. Technically if you're keeping a registry of all objects you could just iterate over your map of id->object and return the key, but this requires you to maintain that map for all objects at all times.

Joshua Suskalo14:10:49

Which perhaps maintaining the map for all objects at all times is fine, depending on how you're doing this, in which case just do that.

Joshua Suskalo14:10:04

And probably prefer a java hash map of some kind to a clojure one for memory reasons.

jpmonettas14:10:36

yeah you can always iterate over your current object pool searching with equals for the id, but that doesn't scale for big pools

p-himik14:10:55

Ah, so (list-objects-of-type-a) is actually a hard requirement, from the perspective of an end-user? Huh.

Joshua Suskalo14:10:11

no, that's not what I mean either. On the client side you never can do a direct question of object to id, you only need object->id to list out all objects of some type. So just keep a map for each type that's relevant that gets updated when you create objects and iterate the entire map every time you request all objects. You never need to do a linear search for single items.

Joshua Suskalo14:10:41

this allows you to only ever have oid->object maps

p-himik14:10:46

> you only need object->id to list out all objects of some type ...why not type->objects? How does object->id helps with that?

Joshua Suskalo14:10:14

this is what I'm getting at. You literally never need to have the object be a key because the only time you need an object->id correspondence is when you're listing all objects.

Joshua Suskalo14:10:19

At least from the current problem statement.

p-himik14:10:46

> You literally never need to have the object be a key Right, that's my impression as well, and I'm trying to understand why @U0739PUFQ claims the opposite.

Joshua Suskalo14:10:29

Because of the step you proposed for caching uuids, of "for new objects generate a new id, for objects we've seen before fetch the cached id for it"

p-himik14:10:41

Crap, I ended up confusing myself. 😅

Joshua Suskalo14:10:15

the problem with that approach is that it needs to have an object->id mapping, which jpmonettas is correct about. The way to avoid it is to not do generation of ids lazily so that we can just iterate the list of all ids and know that it will contain references to all objects.

jpmonettas14:10:23

maybe it helps if I explain what I'm trying to do. So the debugger I'm writing instruments any clojure code so when it run it will retain the pointers to all sub expressions values through time (this is our objects pool) After that, the debugger ui (possibly running remotely) should be able to inspect and work with those objects. Like give me all the objects ids under this F function frame. Now pprint me a object with id OID. Etc

Joshua Suskalo14:10:17

Like I'm suggesting above you can have this work by proactively storing every object in a map when it's created and then at the end you can just iterate over the map to get all the objects' ids

Joshua Suskalo14:10:34

this doesn't require you to realize infinite sequences, or otherwise have well-behaved data.

p-himik14:10:06

But that's linear scanning on every single object serialization, no?

jpmonettas14:10:10

> proactively storing every object in a map how would that map look like?

Joshua Suskalo14:10:27

just oid->object, and it doesn't require linear scanning ever unless you're literally listing all objects.

Joshua Suskalo14:10:11

it allows your standard hash table lookup time for objects by id, and you never need to look up ids by object ever because you know that all objects will always be in the map so you can just iterate the ids directly when you want to list objects to the client.

Joshua Suskalo14:10:18

if you have multiple disjoint sets of objects separated by type (or some other identifiable trait) then I recommend having multiple maps to allow listing to be linear with the number of objects of that type rather than linear with objects in the live set.

jpmonettas14:10:49

hmmm I think I get it, and on my indexes (there is some indexing pointing to objects) I should use the id instead of the object pointer

Joshua Suskalo14:10:03

yes, object pointers aren't stable

jpmonettas14:10:07

so is kind of reifying pointers earlier, at storage time

Joshua Suskalo14:10:24

yes, at the time of object creation you generate a stable id, a uuid, and store it.

p-himik14:10:34

> just oid->object, and it doesn't require linear scanning ever unless you're literally listing all objects. Consider this workflow: 1. A client asks for a particular vector of objects 2. The server iterates that vector, generates oid->object for all the objects, stores it somewhere (probably merges into an already existing map), serializes the objects along with their IDs and sends all that to the client 3. The client then asks again for that vector 4. The server now... does what?

Joshua Suskalo14:10:02

your steps are wrong. The generated ids are generated when the vector was created, not when it was requested.

jpmonettas14:10:04

> yes, at the time of object creation you generate a stable id, a uuid, and store it. I think that should work, will have to change my design but I think you are right

Joshua Suskalo14:10:14

This means the server always iterates the oid->object map and returns all keys

Joshua Suskalo14:10:24

no matter if it's the first request or the hundredth

Joshua Suskalo14:10:05

And again, if you have disjoint sets of objects, you keep them in different maps so that you don't have to filter.

Joshua Suskalo14:10:36

If you don't have disjoint sets of objects then you need to use filtering on the value to determine if the key is included, but that's also easy with clojure sequence operations, it's just a bit slower.

Joshua Suskalo14:10:16

In either case an object existing implies it has an id already, so you never need to "look up the id by the object" because if you already have a single object you're working with on the client it means basically by definition that you have its id.

Joshua Suskalo14:10:21

So you just request by id

Joshua Suskalo14:10:01

and on the server you also never request id by object because you never are working with objects outside of a context of also being in a map iteration where the key is also visible.

Joshua Suskalo14:10:45

So yes some operations require linear scans, but this is a hard requirement when you're dealing with objects that have no tractable concept of equality (as infinite sequences don't)

jpmonettas14:10:09

I'll try to implement that pointer reifying on object creation and see how it goes

jpmonettas14:10:54

Thanks @U5NCUG8NR and @U2FRKM4TW for your help! I was stuck and ended up with a possible solution

Joshua Suskalo14:10:20

Glad I could be of some help, I'd love to hear how it goes.

p-himik14:10:07

Maybe it's not a concern for the task at hand, but what about mutable collections? E.g. both a server and a client know the ID of some HashMap. The client already has that map but would like to get its updated state. Ideally, the server would just respond with some representation of that map that includes just the IDs of the objects, so that the objects aren't serialized every time. But how does it know those IDs in this case, given that the map could've been changed in some dark depths?

Joshua Suskalo14:10:53

this is actually part of how the concept of equality breaks down when you throw mutability in.

Joshua Suskalo14:10:50

it's actually why egal, the equality concept that clojure worked from as inspiration, doesn't consider two mutable objects to be equal unless they are identical to each other, and unfortunately there's no way to determine if maps are equal if we can't check if their keys and values are equal (which we can't because equality is not well-behaved in this system).

p-himik14:10:44

That's why my mindset from the get go was about object<->oid and assigning IDs at the point of serialization, as that's how Bokeh deals with it. Confused myself in the middle of the discussion though, because multitasking is bad.

Joshua Suskalo14:10:43

if we have the request for a map send all the oids of the keys and values for the map it expects though you can detect new keys approximately by looking up the oid for a given key, then checking if that object is a key in the map, and then checking the identityHashCode of the value against the identityHashCode of the objects for the oid that got passed for that value.

Joshua Suskalo14:10:31

Right but it's a hard constraint that we can't have objects as keys in a map @U2FRKM4TW because it's a requirement that this system behaves well with objects that do not have well-behaved hash code or equality semantics.

jpmonettas14:10:10

> Maybe it's not a concern for the task at hand, but what about mutable collections? the debugger "at value store time" stores a snapshot if the thing is derrefable, for things like mutable objects aren't supported, but this is a small percentage of clojure codebases

p-himik14:10:53

> it's a hard constraint that we can't have objects as keys in a map You can, indirectly, by using a hash map implementation that does some checks on the object and, if it can't be hashed, uses its identityHashCode.

Joshua Suskalo14:10:19

No, that still doesn't work because you will have collisions that must be resolved with an equality check which is poorly behaved in this system.

Joshua Suskalo14:10:53

> things like mutable objects aren't supported That helps somewhat because it means you don't have to worry about the thing I mentioned above about requesting maps and needing to send the expected key and value ids

p-himik14:10:32

> things like mutable objects aren't supported Ah, huh. In the CLJS thread you mentioned that you wanted for the functionality to work on any JS object though. So either that task was unrelated or I guess the requirements have changed. > that still doesn't work because you will have collisions that must be resolved with an equality check The equality check will be a simple identical?. Why is it poorly behaved?

Joshua Suskalo14:10:56

hmm. That's a good point, I wasn't thinking about using identical? because my brain was on things that would allow stable references over a wire and time.

jpmonettas14:10:08

mutable objects can't be supported because retaining a pointer to them doesn't make any sense since they can change, and coming after to explore the value at that point in time is going to be wrong

Joshua Suskalo14:10:09

General equality is poorly behaved in this system because it doesn't halt.

Joshua Suskalo14:10:18

identity is pretty much fine though.

jpmonettas14:10:43

> In the CLJS thread you mentioned that you wanted for the functionality to work on any JS object though. yes, the id thing I needed for every object. With not supported I mean that the debugger will signal that the value is a mutable one and you shouldn't trust it (or something around those lines)

p-himik14:10:08

> doesn't make any sense since they can change, and coming after to explore the value at that point in time is going to be wrong In the "at value store time" model - yeah, definitely. I'm thinking in the "at value request time" model, so we're talking about different things - here, a client would be able to refresh its knowledge of the state.

jpmonettas14:10:41

yeah, I'm not worried about that functionality because it is not going to be useful for mutable objects doesn't matter how you implement it

p-himik14:10:35

I almost never use debuggers, but I disagree here. Requesting a state, then running some code, then re-requesting that state can be really useful to see what exactly running that code has changed.

jpmonettas14:10:58

so this debugger is all around immutable and derefable values, which I think cover most of the use cases

Joshua Suskalo14:10:26

I am going to say that this sounds a lot like an interactive version of sayid, and maybe could be implemented on top of that with a parser

jpmonettas14:10:52

> I almost never use debuggers, but I disagree here. Requesting a state, then running some code, then re-requesting that state can be really useful to see what exactly running that code has changed but how can you store all the intermediate states of that previous find-twins function for example? you would have to serialize the mutable HashSet everytime, and you never know if you can serialize a mutable object, it can also be incredibly big

Joshua Suskalo14:10:58

ah, I guess technically sayid doesn't track values through forms inside a function

jpmonettas14:10:50

> I am going to say that this sounds a lot like an interactive version of sayid, and maybe could be implemented on top of that with a parser this debugger has existed for like 2 years already (https://github.com/jpmonettas/flow-storm-debugger) and IMHO is much more powerful than sayid

jpmonettas14:10:02

it also supports ClojureScript

Joshua Suskalo14:10:19

Fair enough. I don't really keep up to date on debuggers, especially because while I do use debuggers, I really don't like firing up another debugger window to attach to my process.

Joshua Suskalo14:10:33

I never use portal or flow-storm or rebl or any similar tooling.

jpmonettas14:10:23

I created it because I'm kind of tired of randomly adding print statements, specially for complicated algorithms or deep nested data

Joshua Suskalo14:10:38

That's fair. I usually just use the cider debugger.

Joshua Suskalo14:10:53

Which is about to get a lot better with JDK 19 and monkeypatching core.async to use virtual threads.

jpmonettas14:10:23

flow-storm is exactly like cider-debugger but it also supports time travel, works on clojurescript, various ways of exploring what happened with a execution, including a programatic one, since you have access from your repl to the execution indexes

jpmonettas14:10:52

it can also instrument entire codebases, so you can do stuff like instrument the entire ClojureScript compiler, compile something, and then step over everything that happened, explore execution at different levels, inspect values, etc

jpmonettas14:10:58

all with one command

p-himik14:10:53

> but how can you store all the intermediate states of that previous find-twins function for example? > you never know if you can serialize a mutable object Hmm. My thinking comes from what I've seen with Portal - it can show you some representation of any object. So if you want a similar thing, you need to serialize only that representation and not the whole object. But no idea how it actually tracks whether an object has changed or not. E.g. I think I can expand a hash map, then change it on the server, and try to expand it further on the client - no idea what happens in Portal here.

ghadi14:10:22

Stay tuned. We're working on some interesting stuff.

jpmonettas14:10:04

but if I'm not confused portal is like REBL or Reveal, a way of visualizing a specific value you just tapped

jpmonettas14:10:31

> Stay tuned. We're working on some interesting stuff @U050ECB92 nice!! around debugging?

ghadi14:10:26

in this ballpark. Something that necessitates an object cache/registry

ghadi14:10:53

can’t say much more.

jpmonettas15:10:56

I guess I'll have to wait to the next Conj 😛

p-himik15:10:51

> if I'm not confused portal is like REBL "Yes, but." I was wrong - it doesn't peak into your state over the wire. It either peaks into the state in its own process or serializes values if you work over the wire, so there are limitations in both cases: https://cljdoc.org/d/djblue/portal/0.31.0/doc/guides/shadow-cljs#portalweb-vs-portalshadowremote

👍 1
phronmophobic21:10:18

I'm a little late to the party, but I know clerk offers streaming potentially infinite values to the browser for visualization.

phronmophobic21:10:23

It sounds like you've already got a plan, but it's possible to use infinite lazy sequences as keys with clojure maps if you wrap them first:

(defprotocol PWrapped
  (-unwrap [this]))

(deftype APWrapped [obj]
  clojure.lang.IHashEq
  (hasheq [_]    (System/identityHashCode obj))
  (hashCode [_]
    (System/identityHashCode obj))
  (equals [this that]
    (if (instance? APWrapped that)
      (identical? obj (-unwrap that))
      false))

  PWrapped
  (-unwrap [_]
    obj))

(defn wrap [o]
  (->APWrapped o))


(def my-range (range))
(def my-range2 (range))

(def my-obj-map {(wrap my-range) "my-range"
                 (wrap my-range2) "my-range2"})

(count my-obj-map) ;; 2
(vals my-obj-map) ;; ("my-range" "my-range2")
(get my-obj-map (wrap my-range)) ;; "my-range"
(get my-obj-map (wrap my-range2)) ;; "my-range2"
(get my-obj-map (wrap (range))) ;; nil

phronmophobic21:10:10

I think this works even when System/identityHashCode has a collision:

(defprotocol PWrapped
  (-unwrap [this]))

(deftype APWrapped [obj]
  clojure.lang.IHashEq
  (hasheq [_] 42)
  (hashCode [_] 42)
  (equals [this that]
    (if (instance? APWrapped that)
      (identical? obj (-unwrap that))
      false))

  PWrapped
  (-unwrap [_]
    obj))

(defn wrap [o]
  (->APWrapped o))


(def my-range (range))
(def my-range2 (range))

(def my-obj-map {(wrap my-range) "my-range"
                 (wrap my-range2) "my-range2"})

(count my-obj-map) ;; 2
(vals my-obj-map) ;; ("my-range" "my-range2")
(get my-obj-map (wrap my-range)) ;; "my-range"
(get my-obj-map (wrap my-range2)) ;; "my-range2"
(get my-obj-map (wrap (range))) ;; nil

jpmonettas01:10:49

oh that is another interesting idea :thinking_face: , thanks @U7RJTCH6J!

👍 1
roklenarcic13:10:35

Does anyone have a snippet how to use rewrite-clj to deep merge clojure data into the original EDN document? I guess I’ll need to use zipper and descend the tree…

lread13:10:05

Depending on what you are doing, https://github.com/borkdude/rewrite-edn might be interesting. Drop by #rewrite-clj if you have questions.

roklenarcic07:10:12

implemented it successfully

roklenarcic07:10:11

(defn- map-node? [zloc]
  (some-> zloc z/node n/tag (= :map)))

(defn- children
  "Convert a :map zloc to a map of k -> zloc of val"
  [zloc]
  (take-while some? (iterate z/right (z/down zloc))))

(defn deep-merge
  "Deep merge 2 zippers."
  [zloc override-zloc]
  (if (and (map-node? zloc) (map-node? override-zloc))
    (reduce
      (fn [zloc [k v]]
        (let [k (z/sexpr k)]
          (->> (if-let [prev-v (z/get zloc k)] (deep-merge prev-v v) v)
               z/node
               (z/assoc zloc k))))
      zloc
      (partition-all 2 (children override-zloc)))
    (z/replace zloc (z/node override-zloc))))

roklenarcic07:10:25

if you can simplify that let me know

Justin Hill18:10:48

Howdy folks, anyone know if it's possible to get permission to the Clojure Jira? I'm from Amazon/AWS and I want to be able to comment on an issue related to maven credential customization (for CI use cases) https://clojure.atlassian.net/browse/TDEPS-99 I tried creating an account, and it created successfully, but the account doesn't have permission to access Jira

hiredman18:10:28

there is a new tools.deps feature that might take care of this https://clojurians.slack.com/archives/C6QH853H8/p1663368713858269

👀 1
hiredman18:10:42

the CLJ_JVM_OPTS environment var

hiredman18:10:08

I guess that hasn't been released yet, but the next version

Justin Hill18:10:52

Oh yeah, that could give me a much nicer workaround than patching the clojure script

Alex Miller (Clojure team)18:10:16

it's been released

🙌 1
Alex Miller (Clojure team)18:10:28

in the future, the best way to start this convo is either with a question in #tools-deps or on https://ask.clojure.org !

Justin Hill19:10:10

Thanks Alex! First time trying to level up from user to question-asker, haha

Erick Isos23:10:33

Hey people! Maybe this is a noob question, but do you know how do we define deprecation warnings on clojure?

Darin Douglass23:10:46

It depends on who your audience is. Sometimes some metadata attached to the var works (defn :deprecated something []). Sometimes you need a little more visibility, like how core.memorize does it https://github.com/clojure/core.memoize/blob/master/src/main/clojure/clojure/core/memoize.clj

Darin Douglass00:10:03

Clojure core does something like the former as well (just search through clojure.core’s src code)

lread04:10:11

A common convention is to use the :deprecatedhttps://guide.clojure.style/#deprecated.

souenzzo10:10:22

(defn ^:deprecated foo [..]..) (defn ^{:deprecated true} foo [..]..) (defn ^{:deprecated "1.0.0"} foo [..]..) (alter-meta! #'foo #(assoc % :deprecated true))

🙌 1