-
Notifications
You must be signed in to change notification settings - Fork 23
Request for comments: Methods as properties on POJOs
I'm adding a new metaobject protocol operation, "dyn:call" that can be used to directly call an object that is, well, callable. One obvious target for this operation are the (currently package-protected) DynamicMethod objects that the BeansLinker internally uses to represent Java methods, both instance and static ones. The advantage of going through the MOP for "dyn:call" instead of just calling raw method handles is that DynamicMethod has two subclasses: SimpleDynamicMethod that wraps a single method handle of a non-overloaded method, and provides type conversion and vararg handling for it, and OverloadedDynamicMethod that wraps a set of overloaded methods, and provides overload resolution, plus the type conversion and vararg handling that the simple variant does. By being able to apply the "dyn:call" operation to these objects, you retain all this helpful functionality after you somehow obtained a DynamicMethod object by name.
This would allow operations like this in a hypothetical JavaScript implementation that knows to compile "Function.call" down to "dyn:call":
// Retrieve "println" as a function
var println = Java.type("java.lang.System").out.println
Function.call(println, out, "The answer is")
Function.call(println, out, 42)
So far, so good. Where the question comes in: how should these instances be accessed? The possibilities are varied, and actually take us into land of API and MOP design decisions far outreaching this original question. It should be a fun ride, bear with me.
One somewhat obvious approach (one used by the example above) is to simply expose them as POJO properties, no fuss. Therefore, "dyn:getProp:println" executed on an instance of "java.io.PrintStream" will return an overloaded dynamic method, that if subsequently used as the receiver of the "dyn:call" operation, can be invoked.
While very appealing on the surface, the approach has the small issue of namespace confusion. Consider this Java class:
public class Confused {
public int getColor() { ... }
public void color()
}
Quick, what should "dyn:getProp:color" operation do when applied to an instance of Confused? Should it invoke "getColor()" and return its value, or should it return a DynamicMethod representing the "color" method?
Note that we don't have this confusion with method invocation operations as "dyn:callPropWithThis:color" will invoke "color()", and both "dyn:callPropWithThis:getColor" and "dyn:getProp:color" will invoke "getColor()".
You could argue that a Java class with an API like above is horrible, and I would agree with you. On the other hand, I don't feel like being a judge of API design and constrain the Dynalink's POJO MOP to a subset of classes that expose a "well-designed API".
I could deliberately not expose these objects in the MOP at all, that is, none of the existing "dyn:" operations would retrieve such objects, ever. However, if you want this functionality in your own language, I could give you a Java API in Dynalink, maybe BeansLinker.getDynamicMethod(Object obj, String name)
, and you'd be at liberty to peruse this any way you wish in your own language linker. You could expose the functionality in some explicit object that acts as a gateway to the underlying Java platform (I use an object named "Java" throughout my examples), i.e.
var out = System.out
// Here's how you'd get a function and call it
var println = Java.getMethodAsFunction(out, "println")
Function.call(println, out, "Hi on stdout!")
Function.call(println, System.err, "Hi on stderr!")
// You could do receiver-bound functions too!
var printlnToOut = Java.getBoundMethodAsFunction(out, "println")
printlnToOut("The answer is")
printlnToOut(42)
You could add a small specialized linker either before or after BeansLinker (depending on what behavior you want to give priority to) that answers "dyn:getProp:" requests, and delegates to this API. This would also allow your language to make other informed decisions, such as whether a Java method retrieved through "dyn:getProp" on an object should be bound to that object or not. Again, language semantics remain more flexible. I could actually provide you with a prefabricated "MethodsAsPropertiesLinker" that you can just drop into your linker chain at the appropriate point.
Admittedly, I like this option best as this feels most like giving language implementers a useful tool, and letting them decide how to use it.
We could also expose them through another object accessible as virtual properties. I.e. we could add virtual properties methods
to objects. Therefore, you could obtain these dynamic methods i.e. as
var println = somePrintStream.methods.println
or
var getSignatureInstance = Java.type("java.security.Signature").methods.getInstance
While this exposes them through the MOP (you'd basically do a "dyn:getProp:methods" on an object, then "dyn:getProp:println" on that (Dynalink-internal) object. The advantage of this is that they get readily exposed through the MOP, and your language doesn't need to do anything to access them. The drawback is that it is a bit more verbose, and still allows for a name clash, most notably with "java.lang.Class.getMethods()" method, as the value of "dyn:getProp:methods" executed on a Class object becomes ambiguous.
I could also invent a new MOP operation, i.e. "dyn:getMethod:", so you could emit a call site like "dyn:getMethod:println Object(Object)". Or two operations: "getMethod" and "getBoundMethod". While somewhat appealing, it doesn't help with languages that don't want to distinguish between properties on an object and the methods, as they'd need to decide at bytecode generating time exactly what it is that they want to retrieve: a property, or a method. I also generally don't want to extend the MOP vocabulary too much towards too specific operations, except if I allow for composite operations.
If I were to allow "dyn:getMethod", then one way to overcome the problem of both namespace clash and selecting the appropriate MOP operation at bytecode emission time is to extend the semantics of MOP operation names a bit, allowing specification of multiple operations at a single call site, in order of preference, and letting the linkers return the first one that they know how to do. If I were to codify this, you could use an operation like this: "dyn:getProp|getMethod:println", which the linker would interpret as "get property println, if there's no such thing, get the method named println". The top-level DynamicLinker object could even automate this for you, so your linker would only need to recognize atomic operations; it could query all linkers for the composite operation, and if none answer, query them all for the first operation, then for second operation, etc. This would also practically solve some other language issues, i.e. languages that don't distinguish between properties of objects and elements of container objects, having only a single namespace for both of them, prime example being JavaScript. Such language runtime could then, for an expression obj.foo
emit a call site with operation saying "dyn:getProp|getElem:foo" - or "dyn:getElem|getProp:foo", depending on what it'd perceive to be the correct preference of namespaces. For instance, if it's faced with an expression of the form someJavaMap.empty
; is it now an invocation of someJavaMap.isEmpty()
(if properties precede elements) or someJavaMap.get("empty")
(if elements precede properties)?
Actually, I think I'm starting to like composite operations. As long as I can provide a machinery that gives the linkers a luxury to ignore them if they want to (as the top-level linker will break it down into components and re-query them if no linker answers to the composite operation query), they should be a powerful MOP concept.
Tell me what you think.
You might not have been aware of it, but this wiki is publicly editable, so as long as you're a GitHub user, you can add your comments into this page, in the venerable style of programming discussion pages on the first ever wiki, the C2 WikiWikiWeb. Feel free to go below the horizontal rule at the bottom of this paragraph, all the way past the last comment, add another horizontal rule, and leave your comment. I will preserve those comments without changes as long as they're on topic and the tone is professional. You can add comments in the main text itself too, but I will periodically perform some gardening on those and am not promising anything with regard to those - they might get incorporated, or they might fall victim to landscaping. I encourage you to sign your edits/comments like this -- Attila. (Yes, I know the autorship can be traced back through the page edit history. It's just nice to have it spelled out on the page itself.)
Comments go here
So I'm really an watching enthusiast in this space, so take anything I say with a grain of salt. At OSCON 2011 a bunch of us (Martin O, Ola, Charlie N, Ben Evans, Stuart Sierra etc) sat around a table (I was the scribe ;p) and theorised about a series of implementations like this. It quickly became apparent that the wide range of languages that it could support meant that the most flexible solution would be one that allowed each language designer (from Ioke/Seph --> Gosu) to have most control. So in my mind something like the "Don't expose them in the MOP, expose them in the Dynalink API" option could work well there. However, here's a key question:
"Who are you trying to solve the problem for?"
If the goal is to enable the existing 'major' JVM languages to interoperate easily (JRuby, Scala, Clojure, Groovy, Jython, Java + Rhino/Nashorn) then a less flexible design that reduces the burden for each of those languages might be the right choice. If the goal is to have most of the ~200 languages be able to use this then... -- [karianna]