Welcome! Please see the About page for a little more info on how this works.

+2 votes
in Clojure by

If you have a class/record that implements a functional interface AND IFn, serializing it will throw the same error as CLJ-2880. Here's a repro

(let [^java.util.function.Function my-f
      (reify
        Serializable
        java.util.function.Function
        clojure.lang.IFn
        )]
  (with-open [os (java.io.ObjectOutputStream. (java.io.ByteArrayOutputStream.))]
    (.writeObject os my-f)))

Will throw java.io.NotSerializableException. If you comment out clojure.lang.IFn, this code works.

This is a regression from 1.11.

2 Answers

+1 vote
by
edited by

We have the serializable aspect logged as https://clojure.atlassian.net/browse/CLJ-2880

The workaround here for now would be to let without the type hint that causes the coercion.

by
Makes sense as the fix for the repro, but unfortunately my use case involves passing the reify into a Java library. In that case the “type hint” effect is implicit due to Clojure compiler method selection.
by
edited by
If you are type hinting on the call, you should be able to use param-tags to choose the method instead and the coercion should then not happen at runtime if it's already the correct type. If you want to share a snippet, maybe I can be more specific.
by
Sure - I can try. I don't have an easy runnable repro, but the code looks loosely like this:

    (.satisfies (PAssert/that pcoll)
                (api/sfn [it]
                         <some uninteresting assertions>)))

`PAssert` is https://github.com/apache/beam/blob/master/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/PAssert.java ; `.satisfies` is https://github.com/apache/beam/blob/master/sdks/java/core/src/main/java/org/apache/beam/sdk/testing/PAssert.java#L352 .

`sfn` is just a wrapper around reify:

(defmacro sfn
  [[binding :as bindings] & body]
  {:pre [(= 1 (count bindings))]}
  `(reify
     SerializableFunction
     (~'apply [this# ~binding]
        ~@body)
     IFn
     (~'invoke [this# input#]
       (.apply ^SerializableFunction this# input#))
     (~'applyTo [this# args#]
       (when-not (= 1 (count args#))
         (throw (ex-info "Bad arguments" {:args args#})))
       (.apply ^SerializableFunction this# (first args#)))))
by
At a glance, I guess I'm not sure why you're getting a coercion there as the reified object should be of the target type.

Do you see same with:

(let [f (api/sfn [it] ...)]
  (.satisfies (PAssert/that pcoll) f))

or

(let [f (api/sfn [it] ...)]
  (^[_] PAssert$IterableAssert/.satisfies (PAssert/that pcoll) f))

The emitted bytecode should have a check equivalent to  (if (instance? SerializableFunction f) f (coerce ... )) and that should take the then branch without coercion in all of these cases.

The next level of debugging would be to look at the bytecode but maybe I could repro with another example.
by
oh, this is because it also implements IFn...
by
Smaller repro:

(import [java.util.function Supplier] [clojure.lang IFn])

(def tl (ThreadLocal/withInitial
          (reify
            Supplier
              (get [_] 100)
            IFn
              (applyTo [this args] 201)
              (invoke [_] 200))))

(.get tl)
;; 201 (was wrapped as IFn)

I'll think about this more, this is a good case.
0 votes
by

Logged this as https://clojure.atlassian.net/browse/CLJ-2898 and working through it

ago by
Thank you! Just to add to your repro, this also fails in the reflector path, e.g.

(defn new-tl [x] (clojure.lang.Reflector/invokeStaticMethod ThreadLocal "withInitial" (into-array Object [x])))

(def tl (new-tl
          (reify
            java.util.function.Supplier
            (get [_] 100)
            clojure.lang.IFn
            (applyTo [this args] 201)
            (invoke [_] 200))))

(.get tl)
;; 201 (was wrapped as IFn)

Seems like a similar fix around https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Reflector.java#L595 is necessary.
ago by
thanks! will do that
...