clojure

New to Clojure? Try the #beginners channel. Official docs: https://clojure.org/ Searchable message archives: https://clojurians-log.clojureverse.org/
2021-02-26T00:13:27.228300Z

I am making a Java library that needs to be extended by having custom transit read/write. It works except for the mapBuilder and listBuilder . It works fine without them:

Reader reader = TransitFactory.reader(TransitFactory.Format.JSON, in, this.c.getCustomReadHandlers(), this.c.getCustomReadDefaultHandler());
T data = reader.read();
But fails to compile if I try to call .setBuilders:
Reader reader = TransitFactory.reader(TransitFactory.Format.JSON, in, this.c.getCustomReadHandlers(), this.c.getCustomReadDefaultHandler());
ReaderSPI reader = reader.setBuilders(this.c.getMapBuilder(), this.c.getListBuilder());
T data = reader.read();
with
cannot find symbol
symbol:   method setBuilders(com.cognitect.transit.MapReader<capture#1 of ?,java.util.Map<java.lang.Object,java.lang.Object>,java.lang.Object,java.lang.Object>,com.cognitect.transit.ArrayReader<capture#2 of ?,java.util.List<java.lang.Object>,java.lang.Object>)
location: variable reader of type com.cognitect.transit.Reader
How do I do this in Java? https://github.com/cognitect/transit-clj/blob/700f205781df180c3b4b251341e7f1831f9f71cb/src/cognitect/transit.clj#L310-L316 https://github.com/cognitect/transit-java/blob/8fdb4d68c4ee0a9b21b38ef6009f28633d87e734/src/main/java/com/cognitect/transit/impl/ReaderFactory.java#L118-L125

alexmiller 2021-02-26T00:25:55.229200Z

Why aren’t you using transit-java?

alexmiller 2021-02-26T00:26:27.229500Z

Or are you

2021-02-26T01:28:10.231100Z

I am using transit-java yes 🙂 I making a java lib that uses transit-java. And a Clj lib on top depends on my-java-lib + transit-clj.

emccue 2021-02-26T01:29:55.231500Z

@nha why getCustomReadHandlers?

emccue 2021-02-26T01:30:06.231800Z

if i had to guess you know the types statically

emccue 2021-02-26T01:30:08.232Z

why not just

emccue 2021-02-26T01:30:51.233100Z

public static final Map<Class<?>, ReadHandler> TRANSIT_READ_HANDLERS = Map.ofEntries(...);

2021-02-26T01:31:04.233400Z

I don’t know the types statically - they are open

emccue 2021-02-26T01:31:13.233700Z

wait 1 sec

emccue 2021-02-26T01:32:17.234Z

(dogs)

🐶 1
emccue 2021-02-26T01:34:08.234400Z

okay so what do you mean by open

emccue 2021-02-26T01:34:21.234800Z

do you mean your library handles the serialization and people can pass in custom handlers

2021-02-26T01:35:18.235200Z

correct, 1min I’ll expand on what I am doing

2021-02-26T01:37:26.236500Z

I am making a library let’s say “my-java-lib”, and a clojure library built on top of it “my-clojure-lib”. The dependency graph looks like: my-java-lib • transit-java my-clj-lib • my-java-lib • transit-clj These libraries are in the message queues domain. Ideally my-java-lib can be used as a base for my-clj-lib and other JVM languages. So I would like the transit handlers and builders to be “open”.

2021-02-26T01:40:02.238900Z

I am able to set the transit write and read handlers just fine. Something like this in my-java-lib :

public setCustomReadHandler(Map<Class<?>, ReadHandler> readHandler) { ... }
and then later retrieved with getCustomReadHandlers()

2021-02-26T01:40:54.240Z

Then my-clj-lib calls setCustomReadHandler and can “teach” my-java-lib how to serialize/deserialize Clojure Objects. And that works great already…. BUT

2021-02-26T01:42:48.241900Z

instead of Clojure {} I am getting back Java Hashmap in the Clojure lib when reading transit data. I believe the way to fix this would be by calling .setBuilders in a similar way but I cannot seem to be able to write that call in Java

emccue 2021-02-26T01:43:29.242100Z

hmm

emccue 2021-02-26T01:45:03.243200Z

honestly all i would be able to do would be done putting the . at the end and letting IDE auto complete guide my hands

emccue 2021-02-26T01:45:51.244Z

though i am curious in what context you do the encoding/decoding

2021-02-26T01:46:48.244700Z

context = domain? I am writing a message queue library

emccue 2021-02-26T01:48:18.244900Z

like

emccue 2021-02-26T01:48:25.245100Z

okay messages are going in the queue

emccue 2021-02-26T01:48:36.245400Z

are they always in the queue as transit?

emccue 2021-02-26T01:48:40.245600Z

like

emccue 2021-02-26T01:49:27.246600Z

.put(Object o) -> o -> transit -> [... transit1, transit2, ...] -> .get() -> transit -> o -> Object o

emccue 2021-02-26T01:49:57.247200Z

and if so, what is the utility of having them be in transit in the queue?

emccue 2021-02-26T01:50:50.248100Z

like - what does your library do that makes it make sense to pick a particular data format for being in transit (pun unavoidable)

2021-02-26T01:53:04.249300Z

> are they always in the queue as transit? Yes > like - what does your library do that makes it make sense to pick a particular data format for being in transit (pun unavoidable) I guess you could pick any format but transit makes it convenient to avoid repetitive serialization/deserialization without having to define a schema

emccue 2021-02-26T01:59:00.249800Z

why not raw bytes and let the user do their own encoding?

2021-02-26T02:05:12.254100Z

(Thanks for thinking about this with me btw 🙂 ) It could be an option to have bytes[], and let the user do it’s own. But I was thinking that by pushing a preferred unified encoding the user experience would be better (and it would avoid the classic JSON + custom ser/deserialize transparently) - with maybe an escape hatch for raw bytes.

emccue 2021-02-26T02:07:48.255Z

i mean, if you want to let java programmers use it you also need to consider the culture

emccue 2021-02-26T02:08:39.255800Z

it will be easier for people to include this in a project if they don't also need to sell people on a data format most people havent hear of

emccue 2021-02-26T02:10:23.257Z

it is fine if it gives some benefit - like introspection on data in the queue or whatever

2021-02-26T02:11:12.257900Z

I was actually hoping that it would actually be easier to get started with transit hidden underneath because then it is already supporting all the Java primitive types. And writing custom Json encoding/decoding is even worse in Java than in Clojure. I actually thought about custom introspection/filtering too which a unified format makes easier.

emccue 2021-02-26T02:13:19.259500Z

how would json be worse than transit?

2021-02-26T02:13:21.259700Z

That would be the benefit of a unified format, not specifically transit though. For instance Json could work - but only on types that Json knows ofc so no Dates for instance.

emccue 2021-02-26T02:13:38.259900Z

most messages aren't primitives

emccue 2021-02-26T02:14:01.260300Z

unrelated, i've been doodling out a java serde impl

😎 1
emccue 2021-02-26T02:14:14.260700Z

copying the library for rust

emccue 2021-02-26T02:14:30.261100Z

public interface Serializable {
    <Ok, Err extends Exception> Ok serialize(Serializer<Ok, Err> serializer) throws Err;

    static Serializable fromBoolean(boolean b) {
        return new SerializableBoolean(b);
    }

    static Serializable fromChar(char c) {
        return new SerializableChar(c);
    }

    static Serializable fromByte(byte b) {
        return new SerializableByte(b);
    }

    static Serializable fromShort(short s) {
        return new SerializableShort(s);
    }

    static Serializable fromInt(int i) {
        return new SerializableInt(i);
    }

    static Serializable fromLong(long l) {
        return new SerializableLong(l);
    }

    static Serializable fromFloat(float f) {
        return new SerializableFloat(f);
    }

    static Serializable fromDouble(double d) {
        return new SerializableDouble(d);
    }

    static Serializable fromString(String s) {
        return new SerializableString(s);
    }

    static Serializable forNull() {
        return SerializableNull.INSTANCE;
    }

    static Serializable fromUnsignedByte(byte b) {
        return new SerializableUnsignedByte(b);
    }

    static Serializable fromUnsignedShort(short s) {
        return new SerializableUnsignedShort(s);
    }

    static Serializable fromUnsignedInt(int i) {
        return new SerializableUnsignedInt(i);
    }

    static Serializable fromUnsignedLong(long l) {
        return new SerializableUnsignedLong(l);
    }
}

/**
 * Serializes a True/False
 */
record SerializableBoolean(boolean b) implements Serializable {
    @Override
    public <Ok, Err extends Exception> Ok serialize(Serializer<Ok, Err> serializer) throws Err {
        return serializer.serializeBoolean(b);
    }
}

/**
 * Serializes a single character.
 */
record SerializableChar(char c) implements Serializable {
    @Override
    public <Ok, Err extends Exception> Ok serialize(Serializer<Ok, Err> serializer) throws Err {
        return serializer.serializeChar(c);
    }
}

/**
 * Serializes a signed byte. (i8)
 */
record SerializableByte(byte b) implements Serializable {
    @Override
    public <Ok, Err extends Exception> Ok serialize(Serializer<Ok, Err> serializer) throws Err {
        return serializer.serializeI8(b);
    }
}

emccue 2021-02-26T02:14:44.261400Z

public interface Serializer<Ok, Err extends Exception> {
    Ok serializeBoolean(boolean b) throws Err;

    Ok serializeU8(byte b) throws Err;
    Ok serializeU16(short s) throws Err;
    Ok serializeU32(int i) throws Err;
    Ok serializeU64(long l) throws Err;

    Ok serializeI8(byte b) throws Err;
    Ok serializeI16(short s) throws Err;

emccue 2021-02-26T02:14:47.261600Z

...and so on

emccue 2021-02-26T02:14:53.261900Z

SerializeSequence<Ok, Err> serializeSequence() throws Err;

    SerializeMap<Ok, Err> serializeMap() throws Err;

    SerializeObject<Ok, Err> serializeObject(String name) throws Err;
    SerializeObject<Ok, Err> serializeObjectVariant(String name, String variantName, int variantIndex) throws Err;
}

emccue 2021-02-26T02:17:19.262500Z

import ser.Serializable;
import ser.Serializer;

public record Apple(int size, String color) implements Serializable {
    @Override
    public <Ok, Err extends Exception> Ok serialize(Serializer<Ok, Err> serializer) throws Err {
        return serializer.serializeObject("Apple")
                .serializeField("size", Serializable.fromInt(size))
                .serializeField("color", color == null ? Serializable.forNull() : Serializable.fromString(color))
                .end();
    }
}

2021-02-26T02:19:42.262700Z

Oh cool. > most messages aren’t primitives You mean that in your experience they are mostly simple java objects like Apple in your example?

emccue 2021-02-26T02:20:15.263200Z

they are usually java objects

emccue 2021-02-26T02:20:36.263700Z

in what i've done they have sometimes been "flat", but most of the time they had at least one list of things attached

emccue 2021-02-26T02:21:33.264400Z

but its always been value classes/pojos/data carriers/dtos whatever

emccue 2021-02-26T02:23:50.265400Z

like, one use case i had was syncing full "articles" between services

emccue 2021-02-26T02:24:19.266Z

and i wrote a few value classes using immutables in java for that to decode json that came on the wire

emccue 2021-02-26T02:24:45.266600Z

another was more simple "chat messages" for live delivery - that was just text with metadata

emccue 2021-02-26T02:24:59.266900Z

i am def. not the person to ask about that domain though

2021-02-26T02:27:41.267900Z

Funny that 😉 I definitely saw and wrote some code passing “articles” or “chat messages” through some queues before. Mostly in Clojure and JS though, not so much in Java

2021-02-26T02:34:45.269300Z

Alright thanks a lot for your input @emccue 🙂 Getting late here, I need to think about it more but will leave it for now

kwrooijen 2021-02-26T14:09:18.272100Z

Hey all. I remember there being a website where you could write the input and the expected output of a function. It would then search the core library for any matching functions. Anyone know what I'm referring to? I'm looking for a function that does the following:

Input:  (1 2 3 4)
Output: '((1 2) (2 3) (3 4))

adam-james 2021-02-26T14:11:17.272700Z

Not sure about the site, but that looks like :

(partition 2 1 [1 2 3 4])

kwrooijen 2021-02-26T14:11:51.273Z

Thanks, that's the one 🙂

🙌 1
tvirolai 2021-02-26T14:13:05.273400Z

The site was probably this one https://borkdude.github.io/re-find.web/

kwrooijen 2021-02-26T14:15:55.273900Z

Bookmarked, thanks 😄

alexmiller 2021-02-26T15:16:07.275100Z

if something seems like an area for performance concern, posting a repro (as minimal as possible) on https://ask.clojure.org would be appreciated

👍 1
alexmiller 2021-02-26T15:16:25.275600Z

we have found and fixed hot spots in macro expansion before

NoahTheDuke 2021-02-26T15:39:21.277100Z

in thread, i'll put the function that had previously been a macro that is called 3k times

NoahTheDuke 2021-02-26T15:39:33.277200Z

(defn click-prompt
  "Clicks a button in a prompt. {choice} is a string or map only, no numbers."
  [state side choice & args]
  (let [prompt (get-prompt state side)
        choices (:choices prompt)]
    (cond
      ;; Integer prompts
      (or (= choices :credit)
          (:counter choices)
          (:number choices))
      (when-not (core/process-action "choice" state side {:choice (Integer/parseInt choice)})
        (is (number? (Integer/parseInt choice))
            (expect-type "number string" choice)))

      (= :trace (:prompt-type prompt))
      (let [int-choice (Integer/parseInt choice)
            under (<= int-choice (:choices prompt))]
        (when-not (and under
                       (when under (core/process-action "choice" state side {:choice int-choice})))
          (is under (str (side-str side) " expected to click [ "
                         int-choice " ] but couldn't find it. Current prompt is: n" prompt))))

      ;; List of card titles for auto-completion
      (:card-title choices)
      (when-not (core/process-action "choice" state side {:choice choice})
        (is (or (map? choice)
                (string? choice))
            (expect-type "card string or map" choice)))

      ;; Default text prompt
      :else
      (let [choice-fn #(or (= choice (:value %))
                           (= choice (get-in % [:value :title]))
                           (same-card? choice (:value %)))
            idx (or (:idx (first args)) 0)
            chosen (nth (filter choice-fn choices) idx nil)]
        (when-not (and chosen (core/process-action "choice" state side {:choice {:uuid (:uuid chosen)}}))
          (is (= choice (first choices))
              (str (side-str side) " expected to click [ "
                   (if (string? choice) choice (:title choice ""))
                   " ] but couldn't find it. Current prompt is: n" prompt)))))))

NoahTheDuke 2021-02-26T15:40:14.277400Z

i'm unsurprised it's slow and don't think it's worth pursuing a reproducible build because of the size/number of uses

alexmiller 2021-02-26T15:40:20.277600Z

how much was being emitted in the macro case? that whole thing?

NoahTheDuke 2021-02-26T15:40:55.277900Z

yeah, change the defn to defmacro and pepper backtics and tildes where necessary lol

alexmiller 2021-02-26T15:41:31.278100Z

certainly seems like it should be a function then

👍 1
NoahTheDuke 2021-02-26T15:43:38.278400Z

If there was some way to have is re-throw the failure or something, I could wrap the function call in a macro that merely says (defn click-prompt [& args] '(is (click-prompt-impl ~@args))), so the error would be on the right line but expansion would be small

p-himik 2021-02-26T15:51:04.278800Z

Couldn't you wrap it in try-catch and rethrow the exception from the right place with the code that was generated by a macro? Alternatively, as hiredman has suggested yesterday - just walk up the trace and report the location of interest.

NoahTheDuke 2021-02-26T15:52:05.279Z

i have is calls inside the function, making it hard to not immediately print the error. i could change those, you mean? have them throw instead, and then catch and report the error?

p-himik 2021-02-26T15:56:49.279200Z

If you throw instead of is and move is to the caller of click-prompt, yes.

👍 1
NoahTheDuke 2021-02-26T15:57:02.279500Z

cool, i'll try that out

NoahTheDuke 2021-02-26T18:13:01.280700Z

to follow up on the conversation about the macro, this is what I've ended up with:

(defmacro error-wrapper [form]
  `(try ~form
        (catch Exception ~'ex
          (let [msg# (.getMessage ^Throwable ~'ex)
                form# (:cause (ex-data ~'ex))]
            (try (assert-expr msg# (eval form#))
                 (catch Throwable t#
                   (do-report {:type :error, :message msg#,
                               :expected form#, :actual t#})))))))

(defmacro click-prompt
  [state side choice & args]
  `(error-wrapper (click-prompt-impl ~state ~side ~choice ~@args)))

NoahTheDuke 2021-02-26T18:14:33.281300Z

which lets me write in the validation function click-prompt-imp:

(throw (ex-info (expect-type "number string" choice)
                {:cause `(number? (Integer/parseInt ~choice))}))

NoahTheDuke 2021-02-26T18:16:43.282600Z

slightly wordier than the previous is expressions, but keeps the compilation time around 45 seconds, vs 35 seconds with no macros and 70 seconds with the whole validation function as a macro

👍 1
NoahTheDuke 2021-02-26T18:17:15.282700Z

thanks for the help, both of you!