Tuesday 12 May 2015

CFML: what manner of object should an Exception be?

G'day:
Just before I commence a discussion on the Lucee Language Forum, I thought I'd do a code sample and ask what people think.

The question is stated in the subject line: "what manner of object should an Exception be?"

Here's some code:

// typeValidation

samples = [
    {type="struct", example={}},
    {type="cfmlObject", example=new Sample()},
    {type="javaObject", example=createObject("java", "me.adamcameron.miscellany.Sample")},
    {type="javaException", example=createObject("java", "java.lang.Exception").init("Message")}
];
try {
    1/0;
}catch(any e){
    samples.append({type="cfmlException", example=e});
}

tests = ["isStruct", "isObject"];

samples.each(function(sample){
    writeOutput("<hr><h2>#sample.type#</h2>");
    tests.each(function(test){
        var result = (evaluate("#test#(sample.example)")) ? true : false;
        writeOutput("#test#(): #result#<br>");
    });
});

This loops over some sample objects, and for each of those tests whether the object passes a test. I'm testing for isStruct() and isObject(). Note I threw a cheeky evaluate() in there ;-)

On ColdFusion, the output is this:


struct

isStruct(): true
isObject(): false


cfmlObject

isStruct(): true
isObject(): true


javaObject

isStruct(): false
isObject(): true


javaException

isStruct(): false
isObject(): true


cfmlException

isStruct(): false
isObject(): true


Notice how a CFML Exception is an object, not a struct. Same as a Java Exception (and a Java object, and a CFML object). That makes sense.

On Lucee, the output is the same except this bit:

cfmlException

isStruct(): true
isObject(): false


This is the point of contention (as per LDEV-323). Micha seems to think an exception should be a struct for some reason. I'm not sure why... and it would simply never occur to me that an exception might be anything other than an object. Exceptions are objects.

Now I'd be completely fine if as well as being an object then the Lucee implementation of Exceptions also implemented some sort of interface (say and interface "StructCompatible" or something) that struct functions except for their "struct" argument. Indeed ColdFusion seems to at least partly do this, as this code works:

structKeyExists(samples[5].example, "type") // that's the CFML exception from above

So ColdFusion accepts an Exception object as an argument for struct functions.

However this doesn't work on ColdFusion:

samples[5].example.keyExists("type")

It errors with:

The keyExists method was not found.

Either there are no methods with the specified method name and argument types or the keyExists method is overloaded with argument types that ColdFusion cannot decipher reliably. ColdFusion found 0 methods that match the provided arguments. If this is a Java object and you verified that the method exists, use the javacast function to reduce ambiguity.

Note if I run that same code on a native CFML struct, it's fine:

samples[1].example.keyExists("type")

(false in this case, as the struct I'm using is empty)

If Adobe had done a complete job here, then a ColdFusion Exception would also implement the methods to make it "StructCompatible". It makes no sense to be able to call the headless function OK, but not the same functionality when called as a method.

Lucee is even weirder. Remember it claims that a CFML exception is actually a struct. Is it now? Check this:

samples[5].example.keyExists("type") // that's the CFML exception from above

Lucee 5.0.0.43 Error (expression)
MessageNo matching Method/Function for lucee.runtime.exp.NativeException.keyExists(string) found
StacktraceThe Error Occurred in
C:\blogExamples\datatypes\objects\typeValidation.cfm: line 27
25:
26: writeOutput(structKeyExists(samples[5].example, "type"));
27: writeOutput(samples[5].example.keyExists("type"));
28: </cfscript>

So... erm... how come one cannot call a struct method on it?

I raised two issues with Lucee:


  • This should be true: isObject(anExceptionObject) (LDEV-323)
  • If structKeyExists() works on an Exception, so should .keyExists() (LDEV-322)

Micha's fixed the latter one (but only in Lucee 4.5 it seems), which is good (-ish).

However the conversation went completely off the rails on the other ticket, with Micha saying some rather odd things. When I suggested an Exception should be an object, and it should implement the StructCompatible interface, he said this:

It just means that your exception objects also need to implement the same interface as structs.
what makes it a struct and make IsObject return false, my point. implementing the struct interface makes an object a struct, implement the array interface a array, ...
(the bold bit is him quoting me inline, for context; the rest is his reaction to same)

"implementing the struct interface makes an object a struct, implement the array interface a array". I'm sorry, but what? Yer joking, right? That's demonstrating a scary lack of understanding of OO, in my view.

For clarification (for Micha on the ticket, and here):

I meant to comment on this:

> implementing the struct interface makes an object a struct

Well, no, it doesn't. That is a really naive understanding of OO, and I can only presume it's lazy wording (or poor translation into English) on your part.

If there's a class C, and its entire behaviour is defined by interface I, it can be said that C is an I, sure. But it's still also a C even though it does not implement any behaviour beyond that which is defined in I.

So if there's a Struct implementation class and a StructAPI interface, a Struct is a Struct and it's also a StructAPI. There would quite possibly be no type references in the Lucee codebase expecting a Struct, but instead a StructAPI is expected (NB: I'm only using the "API" suffix here to make distinct the difference between the implementation and the interface).

Now if there's then a class A and it implements interface I, then it can also be said that an A is an I, but it cannot be said that A is a C simply because A implements the same behaviour as C. For one thing, it also implements all its own behaviour as well.

So if there's a class Exception (behaviour defined by ExceptionAPI) it might also implement StructAPI so that it can share behaviour (method names) with a Struct, and also be used where a StructAPI is expected (as function arguments or return types). This, however, does not make an Exception a Struct. It's still an Exception. It can also be used wherever an ExceptionAPI is expected, and it still has its ExceptionAPI behaviour. If some method was expecting a Struct (which would be bad implementation), then an Exception would not work.

If you're saying you're not re-implementing Exception objects as being actual Struct instances, then you've done the wrong thing. All the Exception class should be doing is implementing the StructAPI interface. But still be a discrete Exception class.

So they should pass both isObject(), isStruct() tests. And if there was an isException() test, they should pass that as well.

The conversation manages to go even further off the rails after that, but it's inconsequential.

Micha's suggested the conversation needs to switch to the Language Forum for some reason, so I'll do that.

But because Micha & I seem to have a different understanding of how interfaces ought to work and the ramifications thereof, I thought I'd get you lot to sanity check things before I go ahead.

So... about my sanity then..?

Cheers.

--
Adam