Apropos of nothing, I'll just point out I'm writing this en route back from Auckland to London, via Kuala Lumpur. This entails a 10 and a 14 hour flight, with a stop to change planes at KLIA (the break is about three hours, I think). It's a bloody long haul. Worse: I did the reverse trip only a fortnight ago, so I've already seem the decent movies they have on offer for Feb (I watched Lincoln, Dredd, Seven Psychopaths, Alex Cross, Taken 2, in decreasing order of quality. The first three were good; the rest watchable). I'm writing this on my wee netbook, which runs for about six hours if I turn the screen brightness down a bit. It's awkward to type on, but it gets the job done. And happily runs CF10, Railo 4.0.2 and CFB simultaneously.
Over the Tasman Sea: one hour out of Auckland
This article is a gripe. You've been warned.Dale Fraser - a person I follow on Twitter - ran into some weirdness with numerically-keyed argument collections the other day, and I started looking into it by way of helping him get to the bottom of it. Whilst investigating, I recalled something Ben Nadel wrote about a coupla years ago, and revisited it. And this is related to his findings in that article. Credit for the basis for this article goes to Dale and Ben. I'm just adding all the extraneous verbiage. Playing to my strengths, one might say ;-)
You probably know that when calling a function in CFML, one can pass arguments in a number of ways. But in case it's not occurred to you, here's a recap.
// named arguments
result = myFunction(one="tahi", two= "rua", three="toru");
// ordered arguments
result = myFunction("tahi", "rua", "toru");
// argument collection
args = {
one = "tahi",
two = "rua",
three = "toru"
};
result = myFunction(argumentCollection=args);
It's handy to have those three options, especially the argumentCollection option, as one can build the struct using conditional logic to decide if a given argument should be passed:
// conditional arguments via argument collection
args = {
one = "tahi",
two = "rua"
};
if (hasThree()){
args.three = "toru";
}
result = myFunction(argumentCollection=args);
All good.
There's one potential option missing from this: passing an argument collection of positional arguments (as opposed to named ones). This requirement could come up if your code is fairly dynamic, and for a given function call you might not know the argument names the function uses, but you know you need to pass a varying set of arguments into the function. Here's some pseudo-code to demonstrate what I mean:
// conditional arguments via positional argument collection (PSEUDOCODE)
if (hasFirst()){
args[1] = "tahi";
}
if (hasSecond()){
args[2] = "rua";
}
if (hasThird()){
args[3] = "toru";
}
result = myFunction(argumentCollection=args);
Looking at this one would quickly think "right, well it makes perfect sense that - in this case - args is an array". Hmmm. Before I start griping, let me digress for a second.
One thing all ColdFusion developers should be aware of is that the arguments scope that a function has is neither fish nor fowl: it seems like a struct, and it also kinda seems like an array. But it's actually neither. This code demonstrates:
// completely vanilla struct
args = {
1 = "tahi",
2 = "rua",
3 = "toru"
};
outputDetails(obj=args, message ="Before" );
// pass said struct into a function
result = checkArgsType(argumentCollection=args);
// re-check what's returned
outputDetails(obj=result, message ="After" );
// simple function which just check's its arguments, and returns 'em
any function checkArgsType(){
outputDetails(obj=arguments, message="Within");
return arguments;
}
// as we do this three times, refactor as a function
void function outputDetails(required any obj, required any message){
writeOutput("#message#<br />");
writeOutput("isArray(): #isArray(obj)#<br />");
try { // we can predict this'll fail on the vanilla struct
writeOutput("arrayLen(): #arrayLen(obj)#<br />");
}
catch (any e){
writeOutput("arrayLen(): #e.message# #e.detail#<br />");
}
writeOutput("isStruct(): #isStruct(obj)#<br />");
writeOutput("structCount(): #structCount(obj)#<br />");
writeOutput("getClass().getName(): #obj.getClass().getName()#<br />");
writeOutput("<hr />");
}
There's a lot there for what I'm wanting to demonstrate, but basically all it does is:
- creates a struct;
- outputs some metadata of the struct;
- passes the struct to a function as an argument collection;
- outputs the same metadata on the arguments scope within the function;
- returns the arguments scope;
- outputs the same metadata on the returned value.
Before
isArray(): NO
arrayLen(): Object of type class coldfusion.runtime.Struct cannot be used as an array
isStruct(): YES
structCount(): 3
getClass().getName(): coldfusion.runtime.Struct
Within
isArray(): NO
arrayLen(): 3
isStruct(): YES
structCount(): 3
getClass().getName(): coldfusion.runtime.ArgumentCollection
After
isArray(): NO
arrayLen(): 3
isStruct(): YES
structCount(): 3
getClass().getName(): coldfusion.runtime.ArgumentCollection
isArray(): NO
arrayLen(): Object of type class coldfusion.runtime.Struct cannot be used as an array
isStruct(): YES
structCount(): 3
getClass().getName(): coldfusion.runtime.Struct
Within
isArray(): NO
arrayLen(): 3
isStruct(): YES
structCount(): 3
getClass().getName(): coldfusion.runtime.ArgumentCollection
After
isArray(): NO
arrayLen(): 3
isStruct(): YES
structCount(): 3
getClass().getName(): coldfusion.runtime.ArgumentCollection
There's a few things to pay attention to here:
The summary of the digression is that the arguments scope can be treated as an array. So it stands to reason the incoming argument collection should also be able to be an array. That would be logical.
- a struct is a struct: no surprises;
- the arguments scope claims not to be an array: OK, but;
- we can call array functions on it. Odd.
- It does claim to be a struct, so we can call struct functions on it;
- but in actual fact it's an object of a different class entirely: an ArgumentCollection.
The summary of the digression is that the arguments scope can be treated as an array. So it stands to reason the incoming argument collection should also be able to be an array. That would be logical.
Somewhere over the Outback: four hours in
I couldn't stay awake (my body clock thinks this is currently about 4:30am, NZDT), so had a kip for a while. I'm in the emergency exit seat against the window, which means sleeping is fairly easy (I can't seem to sleep sitting straight - I need to be on my side-ish - so only the window seats really work with me, as I don't like "cuddling up" with the person next to me. Well more they probably wouldn't appreciate it much ;-).OK, so back to the positional-argument argument collection. Can you guess what data type one of these needs to be? An array, right? Because - like - that would make sense. And making it a struct with numeric keys would just be dumb, and no-one with any sense would do that. BUt CF doesn't implement it that way... OK, so have a second guess: what type of data structure does a positional-argument argument collection need to be? Yeah. Adobe decided the way to go there was to use a struct. The mind boggles.
What's even better is that it barely works.
This is turning into a very code-heavy article, but here's more:
// we'll pass these args into each test function
args = {
1 = "one",
2 = "two",
3 = "three"
};
iterateObject(obj=args, label ="The argument collection");
// call a variety of functions, each with a differing number of arguments defined
noArgsDefined(argumentCollection=args);
oneArgDefined(argumentCollection=args);
allArgsDefined(argumentCollection=args);
extraArgsDefined(argumentCollection=args);
// the functions, as called from above
void function noArgsDefined(){
iterateObject(obj=arguments, label="No arguments defined");
}
void function oneArgDefined(string one ){
iterateObject(obj=arguments, label="One argument defined");
}
void function allArgsDefined(string one, string two, string three){
iterateObject(obj=arguments, label="All arguments defined");
}
void function extraArgsDefined(string one, string two, string three, string four ){
iterateObject(obj=arguments, label="Extra arguments defined");
}
// we call this a few times, so factor it out into a function
void function iterateObject(required any obj, required string label){
writeOutput("#label#<br>");
for (var k in obj){
writeOutput("Key: #k#; value: #obj[k]#<br>");
}
writeOutput("<hr>");
}
So here we have a struct which we use as an argument collection to pass into a bunch of functions, with each function having an increasing number of arguments defined (0, 1, 3, and 4). The functions just dump out their arguments scope. And the results:
The argument collection
Key: 3; value: three
Key: 2; value: two
Key: 1; value: one
No arguments defined
Key: 3; value: one
Key: 2; value: two
Key: 1; value: three
One argument defined
Key: 3; value: two
Key: 2; value: three
Key: ONE; value: one
All arguments defined
Key: ONE; value: one
Key: TWO; value: two
Key: THREE; value: three
Extra arguments defined
Key: ONE; value: one
Key: TWO; value: two
Key: THREE; value: three
Key: FOUR; value:
Key: 3; value: three
Key: 2; value: two
Key: 1; value: one
No arguments defined
Key: 3; value: one
Key: 2; value: two
Key: 1; value: three
One argument defined
Key: 3; value: two
Key: 2; value: three
Key: ONE; value: one
All arguments defined
Key: ONE; value: one
Key: TWO; value: two
Key: THREE; value: three
Extra arguments defined
Key: ONE; value: one
Key: TWO; value: two
Key: THREE; value: three
Key: FOUR; value:
Railo, incidentally, gets this right.
Somewhere over Indonesia
I stopped for a while as I could barely keep my eyes open, so watched a movie: Sinister. It was OK, and had potential to be a bit chilling, but never quite got there. The cabin lights are on again, and it's almost feeding time at the zoo. Battery on 45%.As the cliche goes: "but wait! There's more!". How about if we just have arguments that have numeric names (ie: named arguments with numeric names, as opposed to being an "array" of positional args), mixed in with arguments with string names, eg:
args = {
1 = "one",
2 = "two",
third = "three"
};
include "functionDefinitionsAndCalls.cfm";
The include file here just contains all the code from the previous example, subsequent to the declaration of the args struct. This yields:
The argument collection
Key: THIRD; value: three
Key: 2; value: two
Key: 1; value: one
No arguments defined
Key: THIRD; value: three
Key: 2; value: two
Key: 1; value: three
One argument defined
Key: THIRD; value: three
Key: 2; value: three
Key: ONE; value: one
All arguments defined
Key: THIRD; value: three
Key: ONE; value: one
Key: TWO; value: two
Key: THREE; value:
Extra arguments defined
Key: THIRD; value: three
Key: ONE; value: one
Key: TWO; value: two
Key: THREE; value:
Key: FOUR; value:
Note how the argument values actually get swapped / duplicated unless all the arguments are defined. Sloppy.
Last but not least (well: OK, it actually is least in this case), experimentation with this also shows up a bug in writeDump(). Consider this argument collection struct, which doesn't have contiguous numeric keys:
Last but not least (well: OK, it actually is least in this case), experimentation with this also shows up a bug in writeDump(). Consider this argument collection struct, which doesn't have contiguous numeric keys:
// we'll pass these args into each test function
args = {
1 = "one",
3 = "three"
};
safeDump(var=args, label ="The argument collection" );
// call a variety of functions, each with a differing number of arguments defined
noArgsDefined(argumentCollection=args);
allArgsDefined(argumentCollection=args);
// the functions, as called from above
void function noArgsDefined(){
safeDump(var=arguments, label="No arguments defined");
}
void function allArgsDefined(string one, string two, string three){
safeDump(var=arguments, label="All arguments defined");
}
void function safeDump(required any var, required string label ){
try {
writeDump(var =var, label=label);
}
catch (any e){
writeDump(var =e, label=label);
}
writeOutput("<hr>");
}
I've pared this example back slightly to only call noArgsDefined() and allArgsDefined(), as the behaviour for the other test functions we were calling before don't vary further than these two. The conceit here is that we're error-trapping writeDump() here to demonstrate its bug. The results are:
The argument collection - struct | |
---|---|
1 | one |
3 | three |
No arguments defined - struct | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Detail | [empty string] | ||||||||||||||||||||
ErrNumber | 0 | ||||||||||||||||||||
Message | Variable KEYVALUE is undefined. | ||||||||||||||||||||
StackTrace | coldfusion.runtime.UndefinedVariableException: Variable KEYVALUE is undefined. at coldfusion.runtime.CfJspPage._get(CfJspPage.java:316) at coldfusion.runtime.CfJspPage._get(CfJspPage.java:296) at coldfusion.runtime.CfJspPage._autoscalarize(CfJspPage.java:1522) at coldfusion.runtime.CfJspPage._autoscalarize(CfJspPage.java:1486) at cfdump2ecfm1568701689$funcDUMPSTRUCT.runFunction(E:\cf10_final\cfusion\wwwroot\WEB-INF\cftags\dump.cfm:1961) [etc] | ||||||||||||||||||||
TagContext |
| ||||||||||||||||||||
Type | Expression | ||||||||||||||||||||
name | KEYVALUE |
All arguments defined - struct | |
---|---|
ONE | one |
THREE | three |
TWO | undefined |
I dunno what writeDump() is up to here, but I think I'm in good company: writeDump() itself doesn't seem to know either. Again, Railo doesn't have this issue.
39min out of Kuala Lumpur
Right, so that's about that. There's no big revelation, this is pretty much just some info in case you ever come across this... maybe it'll stick in the back of your mind like Ben's earlier article did for me. I might snooze my way in to KLIA, then seek out a beer. Or two. Or three.And if I find some free wireless, I'll even try to get this dollioed-up so it's suitable for posting, and post it.
Dale has, btw, raised a bug for at least the weirdness with the argument collections with numeric / string arguments... go have a vote if you think it's a shit state of affairs: 3506225.
Righto.
--
Adam