Friday 28 September 2012

Functions and their execution context

G'day:
This article is just a clarification if something I touched on on StackOverflow last night. The question was "how to run a dynamically-named method in CF9?"


Since - I think - CFMX 6 one had been able to access struct keys via associative array notation as well as dot notation:

myStruct = structNew();
myStruct.dotted = "ma";
myStruct["associativeArray"] = "mangu";

The benefit of associative array notation over dotted notation is that the string value of the key can be a dynamic expression:

dynamicKeyValue = "dynamicKey";

myStruct[dynamicKeyValue] = "whero";

Also since CFMX 6 one had been able to reference all scopes as structs (yeah: prior to that it was not possible! Bleah!).

And, indeed, one had also been able to use this notation on objects too, to reference its public methods and properties, eg:

myObj["somePublicProperty"];

This is all very cool, but it wasn't implemented thoroughly. The one omission was that one had never been able to do this:

dynamicMethodName = "getStuff";

myResult = myObj["dynamicMethodName"]();

ColdFusion cannot handle the [](), and errors.

I consider this a bug.

NB: Railo supports this just fine.

Also noteworthy is one can achieve this dynamic-method-calling with <cfinvoke>, but that'd no help in CFScript.

In CF10 Adobe implemented the invoke() function which works OK, but is inelegant (and a bit of a cop-out) compared to just being able to call the method directly.

So then... how to call a dynamically-named method in a script block? There's no nice way of doing it, but there's a couple of hacks.

Extract it

One can do this:

myStaticallyNamedFunction = myObj["myDynamicMethodName"];

myResult = myStaticallyNamedFunction();

This creates a statically-named reference to the dynamically-named method, and then calls the statically-named function.

Insert it

myObj.myStaticallyNamedFunction = myObj["myDynamicMethodName"];

myResult = myObj.myStaticallyNamedFunction();

This does the reverse of the above: it inserts a statically-named reference to the dynamically-named method into the object, and then calls the statically-named one instead of the dynamically named on.

Both are hacky, and neither work perfectly.

The latter leaves a permanent change in the object unless one tidies it up, and there's also a risk of steamrolling an existing method (although this is easily mitigated, I guess).

The former has a more complex caveat.  When one makes a reference to the object's method in the calling code and then executes the method, the context that the method is called in is that of the calling code, not the object.  What does this mean?  It's most easily demonstrated with an example:

// My.cfc
component {

    variables.context = "within the object";

    public string function whichContext(){
        return variables.context;
    }


    public string function somePublicMethod(){
        return somePrivateMethod();
    }

    private string function somePrivateMethod(){
        return "within a private method";
    }

}

<cfscript>
    // context.cfm
    
    param name="URL.dynamicMethod" default="whichContext";

    variables.context = "within the calling code";

    myObj = new My();

    myStaticallyNamedMethod = myObj[URL.dynamicMethod];

    writeOutput("The method is being called in the context of " & myStaticallyNamedMethod());
</cfscript>

This outputs:
The method is being called in the context of within the calling code

This is obvious once one sees the code in front of one's self, but is less obvious when just thinking about it.

A more significant problem here is demonstrated here:

// My.cfc
component {

    public string function somePublicMethod(){
        return somePrivateMethod();
    }

    private string function somePrivateMethod(){
        return "within a private method";
    }

}

<cfscript>
    // privateMethod.cfm

    param name="URL.dynamicMethod" default="somePublicMethod";

    myObj = new My();

    myStaticallyNamedMethod = myObj[URL.dynamicMethod];

    result = myStaticallyNamedMethod();
</cfscript>

This errors our:

The web site you are accessing has experienced an unexpected error.
Please contact the website administrator. 


The following information is meant for the website developer for debugging purposes.
Error Occurred While Processing Request

Variable SOMEPRIVATEMETHOD is undefined.

The error occurred inD:\websites\www.scribble.local\git\blogExamples\methodExtraction\My.cfc: line 6
4 : 
5 :  public string function somePublicMethod(){
6 :   return somePrivateMethod();
7 :  }
8 : 

This is because the function we're calling is no longer in the context of the object, it's in the context of the calling code, so like any other part of the calling code, it has no access to myObj's private methods.

If neither of these potential pitfalls will impact you (which is entirely possible), then the method-extraction approach is tidier, I think.

Of course what would be best is if Adobe finish the work or enabling associative-array syntax to simply support myObj["myMethod"]().



A side note on the method-injection notion.  One can do more with this than just insert statically-named references to dynamically-named methods.  This is a handy this to be able to do:

<cfscript>
    // insertion.cfm

    myObj = new My();

    myObj.getVariables = function(){
        return variables;
    };

    writeDump(myObj.getVariables());
</cfscript>

Which yields:

Variable Scope (of Component)
SOMEPRIVATEMETHOD
Private Function somePrivateMethod
source:C:\Apps\railo-4.0.0.013-railo-express-without-jre\webroot\scribble\git\blogExamples\methodExtraction\My.cfc
arguments
labelnamerequiredtypedefaulthint
return typestring
SOMEPUBLICMETHOD
Public Function somePublicMethod
source:C:\Apps\railo-4.0.0.013-railo-express-without-jre\webroot\scribble\git\blogExamples\methodExtraction\My.cfc
arguments
labelnamerequiredtypedefaulthint
return typestring
THIS
Component (My) 
Only the functions and data members that are accessible from your location are displayed

public
SOMEPUBLICMETHOD
Public Function somePublicMethod
source:C:\Apps\railo-4.0.0.013-railo-express-without-jre\webroot\scribble\git\blogExamples\methodExtraction\My.cfc
arguments
labelnamerequiredtypedefaulthint
return typestring
GETVARIABLES
Public Function closure_1
source:C:\Apps\railo-4.0.0.013-railo-express-without-jre\webroot\scribble\git\blogExamples\methodExtraction\insertion.cfm
arguments
labelnamerequiredtypedefaulthint
return typeany
private
SOMEPRIVATEMETHOD
Private Function somePrivateMethod
source:C:\Apps\railo-4.0.0.013-railo-express-without-jre\webroot\scribble\git\blogExamples\methodExtraction\My.cfc
arguments
labelnamerequiredtypedefaulthint
return typestring

(I needed to demo this in Railo as I don't have CF10 installed at work, and this uses a function expression for the sake of expediency.  It'd work just as well with getVariables() being declared with the function statement).

So this demonstrates one can poke additional methods into an object to do whatever.  I quite often insert a getVariables() method into an object to poke around inside it or call private methods when I'm trying to debug stuff.

And that's about all I can think to say on the topic.

Righto.

--
Adam