This took way longer to revisit than I intended. Apologies to Sean, as it might have seemed like I solicited some code from him and then ignored it. And, indeed, that was mostly the case other than an initial peek and a "hang on... what? How the? But... oh, I think I see..." and a conclusion my assessment of it needed more than 2min.
Right, so a coupla weeks back I wrote and article "Some more TestBox testing, case/when for CFML and some dodgy code". This demonstrated some code (and its tests) that I was messing around with that implemented a
case
/when
/then
/else
function in CFML. That'll make more sense if you read the article.Sean had a look at it and could see some simplifications which could be made, and popped a revised version through to me via Github ("adamcase / case.cfm"). As I alluded to above, my initial parsing of the solution just made my eyebrow raise - Spock-like - and other than that I saw what he was doing, but not how it worked (and it did work; the relevant tests still passed). Today I sat down and had a look at the code, and made myself understand it. And I like it.
Here it is:
// case.cfm
struct function case(){
var requireCondition = function(condition){
condition ?: throw(type="MissingArgumentException")
!isBoolean(condition) && !isCustomFunction(condition) && !isClosure(condition) &&
throw(type="InvalidArgumentException")
}
var requireValue = function(value){
value ?: throw(type="MissingArgumentException")
!isCustomFunction(value) && !isClosure(value) &&
throw(type="InvalidArgumentException")
}
return {
when = function(condition){
requireCondition(argumentCollection=arguments)
if ( isBoolean( condition ) ? condition :
( condition() ?: false ) ) {
return {
then = function(value){
requireValue(argumentCollection=arguments)
var ender = {
end = function(){
return value() ?: javaCast("null","")
}
}
var whenner = { }
var thenner = {
when = function(_){ return whenner },
else = function(_){ return ender },
end = ender.end
}
whenner.then = function(_){ return thenner }
return thenner
}
}
} else {
return {
then = function(_){
return {
when = case().when,
else = function(value){
requireValue(argumentCollection=arguments)
return {
end = function(){
return value() ?: javaCast("null","")
}
}
},
end = function(){ return javaCast("null","") }
}
}
}
}
}
}
}
The chief difference between Sean's version and my version is that he made a very salient observation: my code continues to enforce the validity of the
when()
condition and the then()
(or else()
) value even after the condition had been met and the value had been decided upon. Or similarly I was validating the value of a then()
even when its related when()
condition was false. To clarify, here's an example:result = case()
.when(thisIsFalse).then(value1)
.when(thisIsTrue).then(value2)
.when(thisIsAlsoTrue).then(value3)
.else(value4)
.end()
Here the green bits are things we care about, the red bits are things we don't need to care about.
- we care about the
when()
condition being valid until a condition is met. After that, it doesn't matter, because we have already found our value. So we can safely stop validating or paying any attention to subsequent conditions; - we do not care about the value of a
then()
if the associatedwhen()
condition isfalse
; - in fact the only value we care about is the first one immediately following a
true
when()
condition; or the finalelse()
if no conditions get met. The rest of the time we can ignore them completely. They don't need to be "valid" as we will not be using them.
So Sean uses two different versions of each of
when()
, then()
and else()
.
Also interestingly he dispensed with a lot of my code which determined which functions to return, and in the process made the code a lot tighter. And, thirdly, a side effect of all this is that there's no need to pass-the-parcel with flags showing if the condition had been met, and if they value had been met. He simply remembers which value function was the one from the appropriate
then()
or else()
call, and returns it from end()
. As the when()
and then()
calls only do anything for as long as they need to to find that value, there's no need to tell subsequent when()
/ then()
calls whether they need to execute or not. They can just execute, because they don't do anything. Simple.Let's revisit the code again, with some annotations.
var requireCondition = function(condition){
condition ?: throw(type="MissingArgumentException")
!isBoolean(condition) && !isCustomFunction(condition) && !isClosure(condition) &&
throw(type="InvalidArgumentException")
}
var requireValue = function(value){
value ?: throw(type="MissingArgumentException")
!isCustomFunction(value) && !isClosure(value) &&
throw(type="InvalidArgumentException")
}
Sean's reversed the logic here. I had this:
isBoolean(condition) || isCustomFunction(condition) || isClosure(condition) ? true : throw(type="InvalidArgumentException")
I like Sean's approach because it gets rid of the
?:
and the placeholder true, but has the added bonus of short-circuiting the logic tests. As soon as the condition is deemed valid, that's it. If, on the other hand, we get all the way to the end... the exception still raises. Nice.Those were the easy bits.
return {
when = function(condition){
case()
always needs to return just a when()
. Because the only thing that's valid to call from a case()
is a when()
. One cannot do case().then()
, or case().else()
, or case().end()
. So we need no logic as to which functions to return here.if ( isBoolean( condition ) ? condition :
( condition() ?: false ) ) {
return {
then = function(value){
// [etc]
}
}
} else {
return {
then = function(_){
// [etc]
}
}
}
Here it gets clever. Everything about how things process changes depending on whether the condition is
true
or not. If the condition is true
, we need to grab the value from the following then()
call, and we don't need to do further processing after that, until we get to end()
(which will return this value). However if the condition is false
, our then()
can just be a placeholder (as we already know we're not interested in its value), but we do need to still remember how to do a when()
, then()
, else()
should we have further calls to them which might need to do something.Let's look at the
then()
created by the true path:then = function(value){
requireValue(argumentCollection=arguments)
var ender = {
end = function(){
return value() ?: javaCast("null","")
}
}
var whenner = { }
var thenner = {
when = function(_){ return whenner },
else = function(_){ return ender },
end = ender.end
}
whenner.then = function(_){ return thenner }
return thenner
}
- first it needs to validate its
value
(as per further up) - now a
then()
can be followed by anend()
(when().then().end()
), so we need to define that. Whatend()
will (always) do is to return thevalue()
function's value. Ornull
if there was no condition met, so no value to run. - and if the condition was
true
, we have finished usingwhen()
,then()
andelse()
, so they can just be stubbed. - There's a slight bit of recursion here: subsequent calls to
when()
will still need to return athen()
, and that then will need to returnwhen()
,else()
andend()
. So we use references to build all the eventual responses from the stubbed functions and the realend()
function. It's this bit that did my head in for a while.
Now for the
false
path:
return {
then = function(_){
return {
when = case().when,
else = function(value){
requireValue(argumentCollection=arguments)
return {
end = function(){
return value() ?: javaCast("null","")
}
}
},
end = function(){ return javaCast("null","") }
}
}
}
Here the condition wasn't met, so:
- whilst we do need to return a
then()
function, we don't need it to process, so it's stubbed. - However whilst stubbed, that
then()
does still need to all fully-functional processing to resume with the nextwhen()
call in the chain, and the easiest way to set what that does is... call thecase()
function afresh and use itswhen()
function. Very clever. - We might - at this point - need a functional
else()
function (we didn't on thetrue
path, as it would never be needed, as thewhen()
condition had been met), so it's defined to simply "return" the result of itsvalue
argument (or explicitlynull
if it returns no value)... - ... by implementing an
end()
function which returns itsvalue()
. - Finally the
end()
function which might follow a false-conditionedwhen()
will - intrinsically - not have ever received avalue()
, so anull
can be returned from that too.
--
Adam