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

0 votes
ago 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.

1 Answer

0 votes
ago by

We have this 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.

ago 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.
ago by
edited ago 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.
ago 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#)))))
ago 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.
ago by
oh, this is because it also implements IFn...
ago 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.
...