Search code examples
clojure

Cannot call particular java method from Clojure


I am trying to use the ojAlgo Java library from Clojure, but I am not able to call the weight method of the Expression class.

To demonstrate it, I have a Leiningen project with this dependency: [org.ojalgo/ojalgo "47.3.1"]

I am trying to do this:

(ns ojalgo-test
  (:require [clojure.reflect :as r])
  (:import [org.ojalgo.optimisation ExpressionsBasedModel
            Expression]
           CallExpressionWeight))

(def m (ExpressionsBasedModel.))
;; => #'ojalgo-test/m

(def e (.addExpression m))
;; => #'ojalgo-test/e

(.weight e 1.0) ;; ERROR!

However, the last line fails with the error

  1. Unhandled java.lang.IllegalArgumentException No matching method weight found taking 1 args for class
    org.ojalgo.optimisation.Expression

Question: Why do I get this error and how can I call the weight method without getting an error?

But what is interesting is that I can write a small Java class to call this method:

import org.ojalgo.optimisation.Expression;

public class CallExpressionWeight {
    public static void apply(Expression e, double w) {
        e.weight(w);
    }
}

And that works:

(CallExpressionWeight/apply e 1.0)
;; => nil

Furthermore, I used the clojure.reflect/reflect function to look at the methods of my Expression instance:

(def member-set (set (map :name (:members (r/reflect e)))))
;; => #'ojalgo-test/member-set

(contains? member-set 'setInfeasible)
;; => true

(contains? member-set 'weight)
;; => false

There is something fishy with that weight method...


Solution

  • This seemed very weird to me as well. I thought you might have a version of this library that doesn't match the javadoc, or that you might be compiling your java shim against a different version than your clojure code, but none of these were the case. I created a repo to make it easy to reproduce your problem: https://github.com/amalloy/ojalgo.

    But when I try your code (which anyone can do in that repo via

    lein run -m clojure.main -- -e \
      '(-> (org.ojalgo.optimisation.ExpressionsBasedModel.) (.addExpression) (.weight 0))'
    

    ), I don't get quite the same error you do. Instead, I see

    Caused by: java.lang.IllegalArgumentException: Can't call public method of non-public class: public final org.ojalgo.optimisation.ModelEntity org.ojalgo.optimisation.ModelEntity.weight(java.lang.Number)

    And this makes some sense to me. The Expression you get is actually a subclass of the hidden class ModelEntity, and this class is the one that really defines weight. The clojure compiler doesn't know the static types of the values involved in your interop call, and when it tries to find a method named weight it guesses the one in the class that actually defines weight...but this class is not public, and so it is not permitted to call methods on it reflectively.

    Normally, there would be an easy fix: type-hint the variable with the static type that you want to use:

    (.weight ^Expression e 0)
    

    but in this case even that doesn't work. This has the same root cause as clojure.reflect's inability to find this method. It is a reasonable point of view to say that this method doesn't actually exist! The inheritance hierarchy in this library (which you can investigate with

    javap -cp ~/.m2/repository/org/ojalgo/ojalgo/47.3.1/ojalgo-47.3.1.jar -p org.ojalgo.optimisation.Expression org.ojalgo.optimisation.ModelEntity
    

    ) looks something like this, abridged:

    abstract class ModelEntity<ME extends ModelEntity<ME>> {
       public final ME weight(Number) {...}
    }
    
    public final class Expression extends ModelEntity<Expression> {
    }
    

    The Expression class indeed inherits a member named weight, but because of the unusual stuff they're doing with generics, javac actually generates a synthetic bridge method with a special name that delegates to and from the real weight method. So when the clojure compiler looks for something named weight, the only one it finds is the one in this non-public class that it's not allowed to operate on.

    On the one hand I think it's fair to call this a bug in clojure, but on the other it seems unlikely to be fixed: it depends a lot on implementation details of javac, which could change at any time. I don't think there's anything you can do to convince clojure to call this method, so I think the java shim you've written is the best workaround.