Skip to content

Extending deftype and company for CLR

shoover edited this page Oct 1, 2010 · 12 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) (rename proposed below)
  • gen-interface
  • definterface
  • reify
  • deftype
  • defrecord
  • defprotocol

There are some aspects of the CLR object model that are not yet handled by ClojureCLR. Below, I make some suggestions on ways to extend the current syntax of these macros to accommodate these differences.

There are also a few things I suggest we avoid completely.

Things I think we must do:

  • Rename gen-delegate to something like delegate-proxy (name TBD) to reflect that the parallel is to proxy, not gen-class.
  • Introduce defdelegate to define new delegate types. (Alternate name: gen-delegate.)

I think the following should be done, but realize these are more debatable and seek input:

  • Allow property definition where we now allow method definition (gen-interface and company).
  • Allow property implementation where we now allow method definition (deftype and company, gen-class to be ignored for now).

I think there are some things that we should avoid for now:

  • Do not allow (for now) the definition of new interfaces and classes that have type parameters (true generic types).
  • Ignore indexers (for now).

Implementing methods

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

The signatures that can be indicated need to be extended. The only significant missing piece is properties.

Properties

Extending the syntax to allow properties is optional. The question is whether we allow a natural mode of expression for CLR developers.

If we allow property definitions in definterface and company, certainly we should allow it here. If definterface and company are not extended to properties, it still might be useful here for consistency with externally-defined CLR interfaces.

For properties, we would have to distinguish defining getters and setters. We could do this with something along these lines:

(reify  P1 
  ...
  (m4 :get ... )
  (m4 :set [value] ...)
)

or

(reify  P1 
  ...
  (:get m4 ... )
  (:set m4  [value] ...)
)

It would be simpler to define the getters and setters directly:

(reify P1
  ...
  (get_m4 [] ... )
  (set_m4 [value] ...)
)

However, unless we explicitly define the property m4, the resulting class will not have an m4 property and reflection won’t work properly. I prefer the first syntax because it is explicit.

Proposal: Use the first syntax above to define property getters and setters in signatures when implementing methods.

Defining methods

Again, the missing piece is properties.

Properties

Proposal: Extend the syntax for gen-interface, definterface and defprotocol to allow properties.

We need a syntax distinguishing properties from zero-arity methods. This will take different forms for each of the three.

Here is how properties could be handled in each of the three method-defining forms. Note that this solution assumes a getter and a setter are to be defined for each property.

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

(definterface I1 
  (^String m1 [x ^int y] )                ; normal
  (^String m2 [x (by-ref ^int y)] )   ; taking a by-ref parameter
  (^String m3 [] )                            ; zero-arity method
  (^String m4 )                               ; property)
  )

There is actually a second way to do by-ref for @definterface:

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

For now, I’m sticking with the first version.

Protocols are a bit trickier. Unlike above, we cannot use [] to indicate a property because of confusion with zero-arity methods. Absence of an argument vector is also difficult given defprotocol ’s multiple-arities-per-method-name syntax. We could insist that properties appear separately:

(defprotocol [a b] 
  ( m1 
     [x  y]               ; normal
     [x (by-ref y)]    ; taking a by-ref parameter
     [] )                      ; zero-arity method
                               ; can't overload a property on m1
  ( m1 )               ; property indicated by lack of arg list
)

Or we could use another indicator in place of the argument vector. Here’s one possibility:

(defprotocol [a b] 
  (m1 
     [x  y]                ; normal
     [x (by-ref y)]    ; taking a by-ref parameter
     []                             ; zero-arity method
     :property )               ; property)
)

If this :property solution is used, it might be better to use a similar notation for definterface and gen-interface:

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

(definterface I1 
  (^String m1 [x ^int y] )                ; normal
  (^String m2 [x (by-ref ^int y)] )   ; taking a by-ref parameter
  (^String m3 [] )                            ; zero-arity method
  (^String m4 :property)                               ; property)
  )

Proposal: Use the second version for the defprotocol syntax.

gen-class is problematic

Proposal: Do not extend gen-class to support property definitions.

Given its primary use, there does not appear to be much to gain from allowing property definitions in gen-class. Plus, the syntax would be a pain to modify. IMO, it is not worth the effort.

One example

user=> (definterface I1
                ( ^Int32 m [ (by-ref ^Int32 x) ]))
user.I1

user=> (def r (reify I1
               (^Int32 m [  _ (by-ref ^Int32 x) ] (set! x (int (inc x))) (+ x 12))))
#'user/r

user=> (let [y (int 30)]
              [ (.m r (by-ref y)) y])
[43 31] 

Generics

Definitions of generic types and interfaces

Proposal: Do not allow the definition of generic interfaces and classes.

Should definterface, genclass, and protocol allow the introduction of type parameters so that generic types can be defined?

For now, I say no. I’m willing to revisit this, but I haven’t had time to think through all the complications this is likely to present. There will be syntactic complications for sure, but also issues regarding the instantiating the generic types. Given the use of these mechanisms at the moment, defining non-generic classes will probably suffice.

Indexers

Proposal: Do not implement indexers directly at this time.

Indexers are going to create one more level of syntactic confusion. For now, just use the getters and setters directly.

gen-delegate naming problem

gen-delegate is named parallel to gen-interface and gen-class. This is an incorrect parallel. The latter two define new interfaces and classes, respectively. gen-delegate creates an instance of a delegate. This function is more similar to reify and proxy.

gen-delegate should be renamed to something like reify-delegate, proxy-delegate, delegate-instance, delegate, or something else.

Proposal: Rename @gen-delegate (new name TBD)

However, there is reason to allow a parallel gen-interface and gen-class or deftype and definterface. We may need to create new delegate types on the fly. Either a gen-delegate or a defdelegate should be introduced. (I vote for the latter name.)

Proposal: Introduce a new macro defdelegate to define new delegate types.