Hashcode of a function changing inside an atom ... why is this happening?

As part of the data visualization application I'm working on, I came across something that is either a strange error, or I basically understand nothing.

My application has code that takes data structures representing colors, and converts them to functions that take a number, and returns a hash of RGB color values.

The colors of the gradient and range are implemented:

{:type :gradient :scale [{:bound 0 :r 0 :g 0 :b 0} {:bound 1 :r 255 :g 0 :b 0} {:bound 2 :r 0 :g 255 :b 0}]} {:type :range :scale [{:bound [[< 0]] :r 250 :g 250 :b 250} {:bound [[>= 0] [< 1]] :r 0 :g 0 :b 0} {:bound [[>= 1] [< 2]] :r 255 :g 0 :b 0} {:bound [[>= 2]] :r 0 :g 255 :b 0}}] 

There are functions that turn them into a function whose use resembles the following:

 ((create-colorscale-fn **GRADIENT-MAP**) 1.5) => {:r 128 :g 128 :b 0} ((create-colorscale-fn **RANGE-MAP**) 1.5) => {:r 255 :g 0 :b 0} 

There are functions that convert between them, but this one applies to my post:

 (defn- gradient-colorscale-to-range [in] {:pre [(verify-gradient-colorscale in)] :post [(verify-range-colorscale %)]} {:type :range :scale (into [] (concat (let [{:keys [bound]} (-> in :scale first) {:keys [rgb]} {:r 250 :g 250 :b 250}] [{:bound [[< bound]] :rr :gg :bb}]) (mapv (fn [[a {:keys [rgb]}]] {:bound a :rr :gg :bb}) (partition 2 (interleave (map (partial apply vector) (partition 2 (interleave (map #(vector >= (:bound %)) (-> in :scale)) (map #(vector < (:bound %)) (-> in :scale rest))))) (-> in :scale)))) (let [{:keys [bound rgb]} (-> in :scale last)] [{:bound [[>= bound]] :rr :gg :bb}])))}) 

The check-range-colorscale part of the function checks the following condition for inequality operators:

 (every? #{< <= > >=} (map first (mapcat #(-> % :bound) (:scale in)))) ;;Each bound must consist of either <= < >= > 

Here where my problem is:

For some reason, most of the time when I run this function, it does not give me any problems, and the test for the corresponding inequality operators is performed as follows:

 (def gradient {:type :gradient :scale [{:bound 0 :r 0 :g 0 :b 0} {:bound 1 :r 255 :g 0 :b 0} {:bound 2 :r 0 :g 255 :b 0}]}) (#{< <= > >=} (get-in (gradient-colorscale-to-range gradient) [:scale 0:bound 0 0])) => #object[clojure.core$_LT 0x550b46f1 " clojure.core$_LT_@550b46f1 

However, colorscales are set inside an atom whose contents are inside a global variable. There are editors that I developed to copy part of the color state to another atom, which is then edited using a graphical editor. When I convert the gradient to a range inside the atom, associate the contents of the atom with the global atom, and THEN checks the equality of the operators, for some bizarre reason the test fails.

  (#{< <= > >=} (get-in (gradient-colorscale-to-range gradient) [:scale 0:bound 0 0])) => nil 

When I check that WHY it does not work, it seems that the hash code of the function is smaller than the function, it changes at some point during atomic updates.

 (mapv #(format "%x" (.hashCode %)) [< (get-in @xmrg-cache [[0 0] :colorscale :scale 0 :bound 0 0])]) -> ["550b46f1" "74688dde"] 

And since enabling inclusion seems to test functions based on their hash code, this leads to the failure of my test-range-colorscale test.

So the question is, why does the hash code of my inequality function change during atomic updates? This is a function defined in clojure.core, but it seems like it is being copied at some point?


Edit in response to Piotrek:

The data structure is stored in the global atom in the "inav" namespace.

When loading the hash code <:

  (format "%x" (.hashCode <)) => "425b1f8f" 

When changing the color scale stored in the display configuration configuration from repl using the conversion function:

  (swap! xmrg-cache update-in [[0 0] :colorscale gradient-colorscale-to-range) (format "%x" (.hashCode (get-in @xmrg-cache [[0 0] :colorscale :scale 0 :bound 0 0]))) => "425b1f8f" 

There is a graphical color editor that uses a series of hours to edit temporary copies before updating the active configuration. It started by clicking on the colorscale preview image:

  (.addMouseListener colorscale-img-lbl (proxy [MouseAdapter] [] (mouseClicked [me] (let [cscale-atom (atom (get-in @xmrg-cache [(find-pane-xy e) :colorscale]))] (add-watch cscale-atom :aoeu (fn [kv os ns] (swap! xmrg-cache assoc-in [(find-pane-xy parent-e) :colorscale] ns) (redrawing-function))) (launch-colorscale-editor cscale-atom other-irrelevant-args)))) 

Then in the launch-colorscale editor there are many options, but the corresponding parts are the conversion combobox and the application button:

 (defn- launch-colorscale-editor [cscale-atom & other-irrelevant-args] (let [tmp-cscale-atom (atom @cscale-atom) convert-cb (doto (JComboBox. (to-array ["Gradient" "Range"])) (.setSelectedItem ({:range "Range" :gradient "Gradient"} (:type @tmp-cscale-atom))) apply-button (JButton. "Apply")] (add-action-listener convert-cb (fn [] (let [prev-type (:type @tmp-cscale-atom) new-type ({"Gradient" :gradient "Range" :range} (.getSelectedItem convert-cb))] (when (not= prev-type new-type) (case [prev-type new-type] [:gradient :range] (swap! tmp-cscale-atom gradient-colorscale-to-range) ;other options blah blah ))))) (add-action-listener apply-button (fn [] (reset! cscale-atom @tmp-cscale-atom) (redrawing-function)))) 

Basically, when you click Apply, you copy the contents of tmp-cscale-atom (inside # 'inav / create-colorscale-editor) to cscale-atom (inside let-block in # inav / more-grid-options-dialog), which starts a clock that automatically copies the color scale from cscale-atom to xmrg-cache (globally defined # 'inav / xmrg-cache).

When editing this THIS method, the hash code for <ends up with

 (format "%x" (.hashCode (get-in @xmrg-cache [[0 0] :colorscale :scale 0 :bound 0 0]))) => "5c370bd0" 

A final note about this behavior:

When you call the "redraw function" from INSIDE, listening to the attached button, an attempt to check the scale of the colors will succeed.

When you call the redraw function later from the EXTERNAL application-action listener, an attempt to check the scale of the range scale fails.

... and I just realized the problem, I re-described the color scheme as part of my recheck function called when the color was updated. This is a mess.

+7
hashcode data-structures atomic jvm clojure
source share
3 answers

Functions in Clojure are regular Java objects that implement the clojure.lang.IFn interface. When you load the namespace (including clojure.core ), Clojure will compile the functions (generate a new Java class, instantiate this object and assign this instance as a var value). For example, #'clojure.core/< var will receive a new Java object that implements clojure.lang.IFn , which is less logical.

Clojure does not override the hashCode implementation in the generated function class, which thus inherits the default value from java.lang.Object . Thus, each new instance has its own potentially different hash code. This causes problems: when the namespace is reloaded, vars will receive new instances of functions and, therefore, different hash codes.

On the other hand, I would check how your test works:

  • Are there any namespaces reloaded during the execution of your tests?
  • Do you save a global state (for example, < in a global atom) outside the scope of the test function?

Maybe you should use a local scope for the expected values ​​in your test functions?

+4
source share

I was able to reproduce some of this behavior by explicitly reloading clojure.core and noticing that the hash code of the function changes when the namespace reloads, although the var hash code containing this function does not change when clojure.core .

 user> (.hashCode <) 87529528 ;; jump to clojure.core and reload namespace user> (.hashCode <) 228405583 user> (.hashCode #'<) 1242688388 ;; jump to clojure.core and reload namespace user> (.hashCode #'<) 1242688388 

I can’t say with the code that you have there what is happening during the editing process, which can lead to an overestimation of these forms, so there may be other reasons for this. One way to solve the problem could be to save the var containing your test functions on the map, and not the function object directly. You can do this using the #' reader macro.

Calling the var function as a function automatically calls the function in var, so no changes are required elsewhere.

+3
source share

By the way, last week I noticed a related behavior. When you define identical functions, they do not get the same hash code:

 (defn ink [x] (+ 1 x)) (spyx (hash ink)) (spyx ink) (defn ink [x] (+ 1 x)) (spyx (hash ink)) (spyx ink) (hash ink) => 539734147 ink => #object[tst.clj.core$ink 0x202bb083 " tst.clj.core$ink@202bb083 "] (hash ink) => 757183584 ink => #object[tst.clj.core$ink 0x2d21b460 " tst.clj.core$ink@2d21b460 "] 

So, each defn creates a new function object with a new hash code (in fact, the object label 0x202bb083 is just the hexadecimal value of the hash 539734147 ). This behavior is identical to that observed when creating two separate instances of java Object :

 (hash (Object.)) => 1706817395 (hash (Object.)) => 969679245 

Recall that the Object.hashcode() implementation of Object.hashcode() is simply to get an integer from the object's memory address.

Thus, the result is that we cannot compare function objects for equality, even if they are identical. So, we need a workaround where we store the marker as a map key and the function instance as the corresponding map value. Here is one way:

 (defn ink [x] (+ 1 x)) (defn dek [x] (- x 1)) (def sym->fn {'++ ink '-- dek}) (defn runner [form] (let [[fn-symbol val] form fn-impl (get sym->fn fn-symbol) result (fn-impl val)] result)) (runner '(++ 2)) => 3 (runner '(-- 5)) => 4 
+2
source share

All Articles