Sanity Check For Reassigning Method Arguments In ColdFusion
At PAI Industries, we use the CFWheels framework for our ColdFusion applications. Wheels includes an ORM (Object Relational Mapper) in which the model components live in a dual state. In some cases, they can be instances of a specific entity; and, in other cases, they can be "service objects" that provide static-like methods. This duality requires that I use some new code patterns. And, one thing that I want to sanity check in ColdFusion is what happens when you override an arguments
scope value.
Consider a method, compute()
. In Wheels, if I'm calling this method as a member method, the inputs might be this
-scope properties. But, if I'm calling this method as a static method, the inputs might be arguments
-scope properties. As such, this method has to contain logic that works nicely in either case.
I've been accomplishing this by allowing all of the method arguments to be undefined. Then, internally to the method, I'm re-assigning each value using a variety of fall-backs in a compound Elvis operator assignment expression:
component { | |
public void function compute( string value ) { | |
value = ( arguments.value ?: this.value ?: "Default Value" ); | |
writeDump([ | |
variables: variables?.value, | |
this: this?.value, | |
arguments: arguments?.value, | |
local: local?.value | |
]); | |
} | |
} |
As you can see, internally to the method, I'm re-assigning the value
variable. The chained Elvis operators give precedence to the arguments
scope (for "static" invocation), then the this
scope (for "entity" invocation), and then finally a default value.
The thing I wanted to sanity check is where that value
assignment ends up. I assumed that it simply overwrites the arguments.value
; but, I wanted to put my mind at ease.
To test, I tried invoking this method using both use-cases:
<cfscript> | |
// Invoke as "member method" on instantiated model. | |
e = new Entity(); | |
e.value = "member value"; | |
e.compute(); | |
writeOutput( "<hr>" ); | |
// Invoke as "static method" on model definition. | |
e = new Entity(); | |
e.compute( "hello" ); | |
</cfscript> |
When I run this ColdFusion code, I get the following results:
As member method:
variables
:Empty:null
this
:member value
arguments
:member value
local
:Empty:null
As static method:
variables
:Empty:null
this
:Empty:null
arguments
:hello
local
:Empty:null
In both cases, our chained Elvis operator assignment ends up overwriting the arguments
scoped value, which is what I had hoped it was doing. So, this puts my mind at rest.
But, as I'm writing this, it occurs to me that I can probably simplify this by coding this chained fallback assignment into the argument default itself. In ColdFusion, each optional argument can be assigned a default in the method signature. And, whose to say that assignment can't, itself, be a chained Elvis operator assignment?
Consider this rewrite:
component { | |
// CAUTION: This breaks in ACF (but works in Lucee). | |
public void function compute( | |
string value = ( this.value ?: "Default value" ) | |
) { | |
writeDump([ | |
variables: variables?.value, | |
this: this?.value, | |
arguments: arguments?.value, | |
local: local?.value | |
]); | |
} | |
} |
In theory, this is doing the exact same thing; only, it's moving the assignment into the method signature instead of computing the assignment in the method body. And, this works in Lucee CFML (and in BoxLang 1.0); but, unfortunately, it breaks in Adobe ColdFusion 2023.
Not all is lost, though. Adobe ColdFusion does allow expressions, including method calls, in its default argument assignment. So, maybe we can move this to a private helper method that performs the null coalescing for us:
component { | |
public void function compute( | |
string value = coalesce( this?.value, "Default value" ) | |
) { | |
writeDump([ | |
variables: variables?.value, | |
this: this?.value, | |
arguments: arguments?.value, | |
local: local?.value | |
]); | |
} | |
private any function coalesce( | |
any value, | |
any valueFallback = "" | |
) { | |
return ( value ?: valueFallback ); | |
} | |
} |
All we've done here is move the Elvis operator (?:
) out of the method signature and into a method call. And, when we run this in either Lucee CFML or Adobe ColdFusion, we get the same output as our first experiment:
As member method:
variables
:Empty:null
this
:member value
arguments
:member value
local
:Empty:null
As static method:
variables
:Empty:null
this
:Empty:null
arguments
:hello
local
:Empty:null
Outstanding! I like the way this looks and feels.
Concrete Example
So far, this has been a rather abstract exploration of the ColdFusion mechanics. Let's end with a concrete example. Consider a method that computes a "full name" based on a first, middle, and last name. We can have a Person.cfc
that looks like this - note that Person.cfc
is extending Entity3.cfc
which we just looked at above. This allows it to inherit that coalesce()
method:
component extends = Entity3 { | |
public string function fullName( | |
string firstName = coalesce( this?.firstName ), | |
string middleName = coalesce( this?.middleName ), | |
string lastName = coalesce( this?.lastName ) | |
) { | |
return trim( "#firstName# #middleName# #lastName#" ) | |
.reReplace( "\s+", " ", "all" ) | |
; | |
} | |
} |
This fullName()
method is just concatenating all of the values and removing superfluous whitespace. It uses the inherited coalesce()
method to populate the arguments
scope with this
-scoped values if they exist; or, fallback to the empty string (part of the coalesce()
method signature).
Let's now test this as both a "member method" call and a "static method" call:
<cfscript> | |
p = new Person(); | |
// Invoke as "member method" of instantiated model. | |
p.firstName = "Kit"; | |
p.middleName = "Amanda" | |
p.lastName = "Lobo" | |
writeOutput( p.fullName() ); | |
writeOutput( "<hr>" ); | |
p = new Person(); | |
// Invoke as "static method" of model definition. | |
writeOutput( p.fullName( firstName = "Kit", lastName = "Lobo" ) ); | |
</cfscript> |
If we run this concrete example in both Adobe ColdFusion and Lucee CFML, we get the following output:
- Kit Amanda Lobo
- Kit Lobo
As you can see, when invoked as a "member method", it used the instance "entity properties"; and, when invoked as a "static method", it used the passed-in arguments.
This feels really good to me. I think it's readable and intuitive; and, works nicely with the dual nature of CFWheels models.
Want to use code from this post? Check out the license.
Reader Comments
After posting this, I did one more sanity check with the Adobe ColdFusion code. While this argument signature throws an error:
method( string value = ( this.value ?: "" ) )
... this argument signature works:
method( string value = echoValue( this.value ?: "" ) )
In this case, let's assume that
echoValue()
is a method that just returns whatever was passed to it. So, Adobe ColdFusion will allow the Elvis operator in the method call within a function signature; but, it just won't compile if the Elvis operator is directly part of the default assignment.This just means that we could simplify the code.
But, it's important to remember that Adobe ColdFusion's Elvis operator (
?:
) implementation is buggy and incorrectly treats "empty string" as a Falsy value. Which means, we probably still want to go with thecoalesce()
approach since it means we can normalize the "null value" treatment internally.I filed bug CF-4226518 in the Adobe Bug Tracker for the fact that you can't use the Elvis operator in a default argument assignment expression (without the intermediary method call).
Post A Comment — ❤️ I'd Love To Hear From You! ❤️