Skip to content
Jay Kint edited this page Jun 30, 2021 · 2 revisions

Clojure has been adding a variety of new methods for defining types and interfaces, either directly or indirectly. Here’s a list:

  • proxy
  • gen-class
  • gen-delegate (CLR only -- this may be renamed)
  • gen-interface
  • definterface
  • reify
  • deftype
  • defrecord
  • defprotocol

ClojureCLR implements all of these with extensions to support ByRef parameters, explicit interface implementation and other facets of the CLR object model not present in the JVM model.

Implementing methods

Certain of the macros above — proxy, reify, deftype and defrecord — implement methods (as opposed to something like definterface that declares methods defined elsewhere). They need to indicate the signature of the method being implemented.

The CLR has signature elements not present in the JVM. We extend the syntax of signatures for these macros.

By-ref parameters

Signatures of by-ref methods are handled as in host expressions. (See also ByRef and params parameters.) For example,

(reify  P1 
  (m1 [x #^int y]  ...)             ; normal
  (m2 [x (by-ref #^int y)] ...)   ; taking a by-ref parameter
)

Properties

At present, we do not have a way to specify properties. If you must implement an interface with a property, you must implement the getter and setter methods directly. This means that you will not be able to access the property directly by name on an instance of the type.

Explicit implementation

Explicit implementation is required when you want to give different implementations to methods from different interfaces that have the same signature.

(definterface I1 
   (#^String m1 [#^int x] ))

(definterface I2
   (#^int m1 [#^int x]))

Then the following does not work:

(reify 
  I1 
  (m1 [x] ...)
  I2
  (m1 [x] ...))

One of the two needs to be an explicit implementation:

(reify
  I1 
  (m1 [x] ...)
  I2
  (I2.m1 [x] ...))

Note that the name will have to be fully-qualified.

Here’s a make-me-one-with-everything example:

(definterface I1 
  (^Int32 m1 [^Int32 x ^String y])
  (^Int32 m1 [^String x ^Int32 y])
  (^Int32 m2 [^Int32 x ^String y]))

(definterface I2
  (^Int32 m1 [#^Int32 x ^String y])
  (^String m2 [#^Int32 x ^String y])
  (m3 [x y]))

(deftype T2 [a b]
  I1
  (^Int32 m1 [_ ^Int32 x ^String y]    (unchecked-add x (.Length y)))
  (^Int32 m1 [_ ^String x ^Int32 y]    (unchecked-multiply (.Length x) y))
  (^Int32 m2 [this ^Int32 x ^String y] (unchecked-multiply (int 2) (.m1 this x y)))
  I2
  (m3 [_ x y] (list a b x y))
  (^String user.I2.m2 [this ^Int32 x ^String y] (str y " " x)))

(def x (T2. 10 12)) 

(.m2 x 10 "abc" )                 ;=> 26  (picks up the implicit m2)
(.m2 ^user.I2 x 10 "abc")         ;=> 26 (still picks up the implicit m2, need more typing
(.m2 ^user.I2 x (int 10) "abc")   ;=> "abc 10"  -- picks up user.I2.m2

When using reify and company on the JVM, it is not necessary to implement all the methods in implemented interfaces. The ClojureJVM code does not insert them, and the JVM does not care. Only if you try to invoke an unimplemented method will you discover this, via an exception being thrown.

The CLR cares. If you define a class and do not implement all the methods, either the class must be marked as abstract or you will get an invalid type exception thrown. Abstract classes are useless here, so ClojureCLR must provide dummy implementations of all the methods in all the interfaces being implemented. These dummies throw a NotImplementedException when called.

When generating these dummy methods, we go through the list of all interface methods, subtract the ones the user provided implementations for, and dummy up the rest. Where two interfaces define identical methods (name + arg types + return types), only a single implementation will be provided.

This works except for the case where two interfaces provide methods with the same name and argument types but different return types. One can be implemented directly. One must be implemented via explicit implementation. The problem: which one? We have no way to know, and which we choose determines which interface we must cast to to pick up the desired implementation. Therefore, it is an error if the user does not provide implementations for at least one of the methods in this case.

Defining new methods

Certain of the macros above — gen-interface, gen-class, definterface and defprotocol — define new methods (as opposed to identifying methods in existing interfaces or classes). At present, definterface and defprotocol are defined in terms of gen-interface, so a compatible solutions are desired.

By-ref parameters

ByRef parameters in gen-interface, definterface and defprotocol are specified using the same by-ref syntactic form used in host expressions.

For example,

(gen-interface :name I1
   :extends [I2 I3]
   :methods [ 
     [m1 [Object Int32] String]             ; normal
     [m2 [Object (by-ref Int32)] String]    ; taking a by-ref parameter
   ])

(definterface I1 
  (^String m1 [x ^int y] )            ; normal
  (^String m2 [x (by-ref ^int y)] )   ; taking a by-ref parameter
)

Properties

At present, we do not support properties directly, but properties generate methods of the property name, prefixed by get_ or set_ respectively.