Friday 26 December 2014

CFML: Assignments in conditional statements. Yes or no?

G'day:
I have to bang this out in 18min, as the pub is calling (it's Xmas Eve Afternoon). 17min. Crikey.

So this interesting thread cropped up on the Railo Google Group: "CFIF bug". It so transpires this compiles on Railo:

// test.cfm
a = 1;
b = 2;
if (a=b){
    writeOutput("yup");
}else{
    writeOutput("nup");
}
writeOutput("#a# #b#");

Note the typo: "=" rather than "==".

And it outputs:

yup2 2



This is because a=b is an assignment statement, and those are valid expressions, and indeed in this case the expression evaluates to a valid boolean value so all the code is legit.

However on ColdFusion, one gets this:

Invalid CFML construct found on line 4 at column 6.

ColdFusion was looking at the following text:=
The CFML compiler was processing:

  • A script statement beginning with if on line 4, column 1.
  • A cfscript tag beginning on line 1, column 2.
The error occurred in C:/ColdFusion/11/express/cfusion/wwwroot/test.cfm: line 4
2 : a = 1;
3 : b = 2;
4 : if (a=b){
5 :  writeOutput("yup");
6 : }else{

Note this is a compiler error: having an assignment in a conditional construct is illegal in ColdFusion's implementation of CFML.

This is not because an assignment is not an expression: it is:

a = 1;
b = 2;
c = a=b;
writeOutput(c);

The output here is "2", which is the result of the a=b expression.

Interestingly ColdFusion errors with this though:

a = 1;
b = 2;
writeOutput(a=b);

Parameter validation error for the WRITEOUTPUT function.

A built-in ColdFusion function cannot accept an assignment statement as a parameter, although it can accept expressions. For example, WRITEOUTPUT(d=a*b) is not acceptable.

Railo also errors, but I think gets the error message wrong:

Railo 4.2.1.007 Error (template)
Messagemissing required argument [string] for function [writeoutput]
Patternwriteoutput(string:string):boolean

Railo should be able to cast the result of the assignment expression to a string, no problem. However I dunno what's going on with ColdFusion there.

I know some languages allow assignments in conditional expressions, and some don't. The reason being - I understand - is to specifically prevent people accidentally using "=" (assignment) when they mean "==" (comparison). But let's see how various languages handle it:

JavaScript

// test.js
a = 1;
b = 2;
if (a=b){
    console.log("yup");
}else{
    console.log("nup");
}
console.log(a + " " + b);

Output: yup 2 2

So JavaScript supports it.

Ruby

# test.rb
a = 1
b = 2
if a = b then
    puts "yup"
else
    puts "nup"
end
puts "#{a} #{b}"

Output: yup 2 2

Ruby too.

Python


# test.py
a = 1
b = 2
if a = b:
    print("yup")
else:
    print("nup")

print("{0} {1}".format(a,b))

Python errors with:
File "test.py", line 4
if a = b:
^
SyntaxError: invalid syntax

So it doesn't allow assignments in conditionals like that.

PHP


<?php
// test.php
$a = 1;
$b = 2;
if ($a=$b){
    echo "yup";
}else{
    echo "nup";
}
echo "$a $b";

With PHP it was fine:
yup2 2

(NB: my 18min is up, I did not get the article finished in time before going to the pub. I'm now continuing this on Boxing Day / St Stephen's Day)

Groovy


// test.groovy
a=1
b=2
if (a=b){
    println "yup"
}else{
    println "nup"
}
println "$a $b"

Groovy didn't like this either:

C:\webroots\shared\scratch\blogExamples\cfml\expressions>groovy test.groovy
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
C:\webroots\shared\scratch\blogExamples\cfml\expressions\test.groovy: 4: expecti
ng ')', found '=' @ line 4, column 6.
   if (a=b){
        ^

1 error


C:\webroots\shared\scratch\blogExamples\cfml\expressions>

Java

(note: this code has been updated thanks to Ryan pointing out I was not paying attention to things before):

// Test.java
public class Test {
    public static void main(String[] args){
        boolean a = true;
        boolean b = false;
        if (a=b){
            System.out.println("yup");
        }else{
            System.out.println("nup");
        }
        System.out.println(String.format("%b %b", a, b));
    }
}

This compiles & runs fine:

nup
false false


Go


// assignmentInConditionalExpression.go
package main

import "fmt"

func main() {
    a:=1
    b:=2
    if a=b {
        fmt.Println("yup")
    }else{
        fmt.Println("nup")
    }
    fmt.Printf("%d %d", a, b)
}

Go balked as well:

C:\webroots\shared\scratch\blogExamples\otherLanguages\go\src>go run assignmentI
nConditionalExpression.go
# command-line-arguments
.\assignmentInConditionalExpression.go:9: a = b used as value

C:\webroots\shared\scratch\blogExamples\otherLanguages\go\src>

Summary

So, what's the tally-up?

Can use assignments as conditional expressions: JS, Ruby, PHP, Java
Cannot use assignments as conditional expressions: Groovy, Go, Python
Cannot make up its mind: CFML

To be honest, I'd be fine with CFML going either way. However, I have a coupla qualms:
  • even within each platform, whether or not an assignment is treated as an expression isn't uniformly implemented. It seems OK in an assignment, but not as a function argument, and Railo allows one in a conditional statement, but ColdFusion does not.
  • It needs to be either one approach or the other, on both platforms.  We can't have ColdFusion doing it one way, and Railo doing it another way.
There are definitely some bugs here.

In CFML in general, it seems an assignment is supposed to be an expression, as one can use the result of an assignment expression as the value in another assignment, eg:

b = a=1;

This is actually documented: Expressions - Developing guide. So assignments definitely are supposed to be expressions. But the implementation is just a bit shonky.

It's not possible to use an assignment as a function argument. This fails on both Railo and ColdFusion:

val(a=1);

That's a bug in both. But... what if native CFML catches up with dev-written CFML and built-in functions are changed to allow arg/value pairs? (there is a ticket for this: 3540467, currently closed). This would make it very unclear as to whether a=1 in that expression is an assignment expression or a named argument. I think Adobe need to take a position here, and let us know what it is.

After a bit of research, I notice Railo has worked this a different way. It used the colon operator instead of the equals operator. So this works:

val(value:1);

And, as a result, this also works:

val(value:a=1);

I'm not really a fan of using the colon operator to assign an argument a value, but I guess this covers both bases. However as val() also takes positional arguments, this still ought to work:

val(a=1);

Where the result of a=1 is the value for the value argument.

[messes around a bit]

Oh Christ. What a balls up. Railo has also implemented named arguments with the equals operator. This works:

val(value=1);

This also explains the error message earlier.

This seems to be a case of Railo giving multiple solutions to the same challenge, in an attempt to please everyone. Which I wish they'd stop doing.

Seen in this light, I would guess this should work:

val(value=a=1);

However it doesn't. The parser seems to be getting confused still. I give up.

It is unclear as to whether the intent in ColdFusion is to prohibit assignments in conditional statements, or whether it doesn't work because of thoughtless implementation. And my money could go either way there (it errs towards the latter, that said). So it's unclear if Railo's behaviour is correct here. However in that it differs from ColdFusion isn't great.

How do you think all this should be handled? Let's ignore the differences between ColdFusion and Railo, and just think about how it should work as far as CFML is concerned...

--
Adam