19
$\begingroup$

Novices writing in mainstream languages often don't realise that conditions like x < y are expressions which can occur anywhere an expression is syntactically expected; they may think that such expressions can only occur in the condition of an if statement, while statement or so on.

For example, beginners often write code like this, where return condition; would be simpler:

if (condition) {
    return true;
} else {
    return false;
}

Novices with this misconception also often don't realise that booleans are values which can be compared with ==, so they might write (x < 0 && y < 0) || (x >= 0 && y >= 0) instead of the simpler (x < 0) == (y < 0).

Perhaps that is because in natural languages, we rarely refer to a condition's truth value as a noun. Conditions and the word "if" usually go together in natural language, so simplifying the above might not cross beginners' minds.

But in every programming language I have seen, conditions are expressions like any other, and the boolean type is like any other. They can be used anywhere any other expression or type can be used.

My question: are there any languages which are designed in a way which reflects the beginners' mindset, where conditions are different to other expressions, and cannot be handled in the same way?

  • Such a language might require a construct for handling conditions, which programmers of mainstream languages would consider redundant.
  • They might lack the ability to store a condition's result in a variable.
  • The comparison and logical operators might only be syntactically allowed in the conditions of if statements, while statements or so on.
  • Or if conditions are expressions, some operations (e.g. return) might still not be allowed for booleans, or only allowed for the literal values true and false.
$\endgroup$
10
  • 2
    $\begingroup$ You seem to imply that programmers who don't use "==" as a boolean operator didn't realize it can be done. What can be done is not necessarily a good thing. I would reject such code in a code review because there are boolean operator for that. Code like a == b == c is nonsense. Use a ^ b ^ c instead. $\endgroup$
    – Florian F
    Commented Apr 26 at 9:41
  • 1
    $\begingroup$ @FlorianF There is no such implication, merely a statement that many novices don't realise == is possible to use with booleans. Personally I think (x < 0) == (y < 0) is a lot clearer than !((x < 0) ^ (y < 0)), and your example with three variables only works because the double negation cancels out. $\endgroup$
    – kaya3
    Commented Apr 26 at 10:56
  • $\begingroup$ @FlorianF If I recall correctly, in some strongly typed languages, including Java, you can compare booleans with == but not with ^. $\endgroup$
    – CPlus
    Commented Apr 26 at 18:22
  • 1
    $\begingroup$ @FlorianF In the expression (x < 0) == (y < 0) you are comparing the signs of the two numbers, since that's what the comparisons determine; so it's perfectly sane to use == in this example. I don't read it as an XNOR and I doubt OP does. And to be clear, I didn't choose the example, OP did; and of course they chose an example which demonstrates what they are talking about! Nobody is saying that == should always be used with booleans, the point is that it can be used but beginners often don't realise that. Also why would you write !(... == ...) instead of ... != ...? $\endgroup$
    – kaya3
    Commented Apr 26 at 20:58
  • 2
    $\begingroup$ @FlorianF "Code like a == b == c is nonsense" - Maybe in your preferred language. But not in general. In Python it simply means that a and b are equal and that b and c are equal. Just like you'd write and read "a=b=c" in math. $\endgroup$
    – no comment
    Commented Apr 28 at 12:12

10 Answers 10

19
$\begingroup$

Yes, there are languages where conditional expressions do not work like C and produce values, which I think is what you mean by "normal language" here.

One recent example is Verse, where the conditions of an if are failable expressions — computations that may succeed or fail — and the branch of the statement taken depends on the success, not the value. The conventional motivating example is something like this:

if (Element := MyArray[Index]):
    Log(Element)
else:
    Print("No such element")

but every condition is failable, including relational operators. Anything that could fail — including something like a potential division by zero — must be in a context that allows failure. Those contexts are the if condition and a few other places, and there must always be a failable expression in those. More specifically, failable operations have the transacts effect, and the purpose is that any other side effects within the failure context are rolled back.

Comparisons like the ones in your examples are within the scope of this. < and >= are a failable operations, as is and. These operators don't have useful return values that you can store in a variable, return, or compare to one another: if you did hypothetically run (x < 0) = (y < 0), a failure on either side would preempt the rest of it, and so you can't actually write that. You would always write out the explicit version instead.

The "logic" type is the typical Boolean true-or-false value, distinct from conditions used for control flow. These are real values that you can store and compare, and they can be lifted into failable contexts using the ? operator, which just fails if the value is false. Those can be compared and combined in the way that you want, but getting the base values into that form in the first place is quite verbose.

This model is not enormously common, but it wasn't novel when Verse picked it up. It's also been used in research systems previously, as has the more monadic-value version of the same thing.


Another instance is common in languages built around pattern matching of some kind; SNOBOL has been mentioned in the comments, and there are a number of others whose core approach is "try to match the subject against this pattern, and then do something based on whether it matched or not". It's often possible to combine multiple patterns together (match all/some/none of these), and those patterns could include things like numeric comparisons, but there isn't ever a "result" value — the result is always a direct control-flow change rather than a data value.

This class includes widespread tools like sed, but also a number of embedded domain-specific languages within other tools.


Bespoke templating languages, especially those intended for end-user use, often have quite limited and specific control-flow structures that embed the test operations within themselves. A construct like (x < 0) == (y < 0) is liable to be non-obvious to an end user, and treating a decision as a value is more abstract than desired here. There will be either specific decision combinators only or control structures that internally integrate multiple branches (IF ... AND ... THEN ...). You've likely also encountered this approach in user interfaces for building up complex conditions in non-programming application software, which are reductive cases of visual programming languages.

A Factorio screenshot with conditions OR(AND(Empty, 5s inactivity), 30s passed)


One more concern that can come up is in languages that decouple Boolean truth and falsity from the true or false values themselves: these values may be true or false when used as conditions, but not be equal to other values in the same category.

  • Relational operators may return one of their values in some cases to allow chaining, or else some false-coalescing value; these return values are naturally not equal.
  • Types may be able to have user-defined coercions to Boolean that do not align with equality of the original values; many languages have these, including dynamically-typed Python and statically-typed C#.
  • Yes-or-no results may exist in a separate space from data values, such as only being implicit results of an action; some command languages work this way.
  • The language may let any individual value be given a Boolean meaning; Raku allows 0 but True to do this for any individual value.

It's often not impossible to do what you're wanting in these languages, but it isn't the simple syntax-driven transformation you've given.

$\endgroup$
12
  • 1
    $\begingroup$ This seems more like Java's try(var element = ...) { ... } catch { ... }, or Rust's if let Some(element) = ... { ... } else { ... }, than a conventional if statement. I wouldn't call Element := MyArray[Index] a condition in the sense of this question; it can't be composed with && and ||, for example. $\endgroup$
    – kaya3
    Commented Apr 26 at 1:36
  • $\begingroup$ @kaya3 Throwing an exception is also a kind of effect, and you could transpose a lot of this model into e.g. Java by turning all your ifs into trys with the then block below the conditions and the else block in a catch, and making all your comparison operations throw a checked exception. You wouldn't get the transactional behaviour out of that, though, since mutations that had already happened would persist, and non-failable conditions would be permitted. It's not hugely similar except that you can do a partial translation between these effects. $\endgroup$
    – Michael Homer
    Commented Apr 26 at 1:40
  • 2
    $\begingroup$ Maybe I need to be more explicit that the conditional operators are also failable; the assignment statements are the motivation for them having this, while the conditions are the impact for this question. $\endgroup$
    – Michael Homer
    Commented Apr 26 at 1:42
  • $\begingroup$ If I understand correctly, conditions like x < y are more analogous to if(!(x < y)) { throw ...; } in traditional languages? Could I write x < y by itself for the purpose of asserting a precondition, for example? $\endgroup$
    – kaya3
    Commented Apr 26 at 1:53
  • 4
    $\begingroup$ This sounds a lot like SNOBOL and ICON. These languages have a special "fail" value rather than boolean type. So comparisons either fail or return their right operand, allowing things like A < B < C. You can't store a failure in a variable -- you need to catch it and branch to a label (SNOBOL) or use an "if" or "while" (ICON) that does the control flow based on the fail. $\endgroup$
    – Chris Dodd
    Commented Apr 26 at 6:12
18
$\begingroup$

This is true in Bourne Shell, and most of the shells that are derived from it, such as POSIX shell, bash and ksh. There are no first-class boolean values, condition testing is done by testing the exit status of a command. The exit status of the last command is stored in the variable $?, but you can't just test this directly, you have to write another command to test it, e.g.

if some_program; then do_something; fi

is equivalent to

some_program
if test $? -eq 0; then do_something; fi

We have to use the test command (which has the synonym [, giving the illusion that it's part of the syntax of if).

We can also write it as

some_program && do_sometihng

but this still isn't an example of boolean values; the && and || operators for boolean expressions are like if in that they test the exit status of the command. You can't write something like $? && do_something because $? isn't a command.

$\endgroup$
2
  • $\begingroup$ Traditional C doesn't have booleans either, just various integer types. But what it does have, is builtin comparison operators and implicit interpretation of the integers into truth values for if and such. That comparison as part of the language is what sh doesn't have, instead having test/[ is a command same as any other. $\endgroup$
    – ilkkachu
    Commented Apr 28 at 20:14
  • $\begingroup$ @ilkkachu While it doesn't have a distinct boolean type, any integer type acts as a first-class boolean value because it can be stored and passed around as a value. $\endgroup$
    – Barmar
    Commented Apr 28 at 21:28
12
$\begingroup$

Many computer architectures work like this at the level of machine code / assembly language. There may be a "condition code" as part of the machine state, such that conditional branches can only be taken based on the condition code, which is only set by specific compare instructions.


Let's take x86 as an example. (I've simplified many details in what follows, and left the assembly examples un-optimized for clarity.) The special FLAGS register, distinct from the general-purpose registers that hold ordinary integer data, has bits for carry, sign, zero and overflow. The conditional jump instructions Jcc will branch or not according to the values of these bits. For instance, JZ foo will jump to foo if the zero flag is set; JNC foo will jump if the carry flag is not set, and so on. And the flags are set by integer arithmetic and bitwise logic instructions, most notably CMP.

But there isn't a single instruction to conditionally jump based on a boolean value in a general-purpose register or memory. You would first need a compare instruction to set the flags based on the value of the boolean, and then a conditional jump based on the flags. So high-level code like

bool b;
if (b) { stuff; }

would need to be compiled as

    cmp bl, 0   ; alternative: test bl, bl
    je over     ; jump if zero flag set, meaning bl was equal to 0
    ; stuff...
over:

which looks more like

if (b != false) { stuff; }

Conversely, prior to the 386, conditional jumps were the natural way on x86 to materialize the result of a comparison in a boolean variable. If you had high-level code like

int c,d;
bool b;
b = (c < d);

you would typically have to compile it into something like

    cmp cx, dx
    jl was_less
    mov bl, 0
    jmp proceed
was_less:
    mov bl, 1
proceed:

which looks more like

if (c < d) {
    b = true;
} else {
    b = false;
}

The 386 added SETcc which let you set a register to 0 or 1 based on the flag values, so at that point you could instead do

    cmp cx, dx
    setl bl

But still, the result of the comparison has to pass through the flag register before it can be materialized as a 0/1 byte value.


If I have more time later, I'll try to come back and write a little about the pros and cons of this design feature, and mention how some other architectures do it.

$\endgroup$
1
  • $\begingroup$ If in some programming language, "condition" expanded to either "conditionfactor" or "condition OR condition", "conditionfactor" expanded to "innercondition" or "conditionfactor AND conditionfactor" and "innercondition" expanded to either "(condition)", "NOT innercondition", or "value comparator value", conditions could be easily handled by a recursive-descent parser function that received arguments for "target label if true" and "target label if false", without ever having to treat the relational operators as yielding any kind of numeric value. $\endgroup$
    – supercat
    Commented Apr 26 at 20:20
12
$\begingroup$

In Microsoft SQL Server, the Boolean Type is not available as a general-purpose type, and exists only in contexts that require a boolean expression.

This is spelled out in the manual for Comparison Operators:

The result of a comparison operator has the Boolean data type. This has three values: TRUE, FALSE, and UNKNOWN. Expressions that return a Boolean data type are known as Boolean expressions.

Unlike other SQL Server data types, a Boolean data type cannot be specified as the data type of a table column or variable, and cannot be returned in a result set.

In order to represent a boolean property within a table or a Transact-SQL variable, you have to instead use the bit type which is an integer constrained to the values 0 and 1 (and NULL).

A boolean expression cannot be converted directly to a bit or vice versa, so you have to explicitly use comparison operators every time:

DECLARE @IsValid Bit

IF ( @SomeInputNumber > 0 )
BEGIN
   SELECT @IsValid = 1
END ELSE BEGIN
   SELECT @IsValid = 0
END

-- or

SELECT @IsValid = CASE
    WHEN @SomeInputNumber > 0 THEN 1
    ELSE 0
END

-- then later

IF ( @IsValid = 0 )
BEGIN
   -- Error handling
END
$\endgroup$
8
$\begingroup$

This is not uncommon in older versions of BASIC. GW-BASIC, for example, does not have a boolean data type, so there is no way to use a boolean expression except in statements that specifically expect a boolean expression.

This makes it easy for GW-BASIC to reuse the single = operator for both equality expressions and assignment statements.

$\endgroup$
7
  • $\begingroup$ That's also true for SmallBasic. $\endgroup$ Commented Apr 26 at 11:31
  • $\begingroup$ Don't they simply use an integer value instead?  (IIRC, at least some BASICs do that: conditional operators return 0 for false and -1 for true, AND and OR are bitwise, and IF/WHILE treat 0 as false and non-zero as true.  But there's no problem about storing a condition in an integer variable.  I think very early versions of C worked that way, too, before the logical && and || were added, and arguably it's still much like that under the covers.)  If so, I'm not sure that fits the question as asked.  Interesting, though. $\endgroup$
    – gidds
    Commented Apr 26 at 13:26
  • $\begingroup$ @gidds: BBC Basic certainly works the way you describe! $\endgroup$
    – psmears
    Commented Apr 26 at 13:50
  • $\begingroup$ It's been a long time, but I'm pretty sure it was true in the original Dartmouth BASIC. $\endgroup$
    – Barmar
    Commented Apr 26 at 15:14
  • $\begingroup$ @psmears Yes, that's the one I'm most familiar with :-) $\endgroup$
    – gidds
    Commented Apr 26 at 16:06
4
$\begingroup$

csh/tcsh

From the manpages [1] [2]:

Expressions

The if, while and exit builtin commands use expressions with a common syntax. [...] Note that the @ builtin command (q.v.) has its own separate syntax.

I haven't used tcsh much, but from this I gather that the expressions described in this section can only be used for conditionals (and returning an exit status). @ is apparently how you do math on variables, but like it says right there, it's a separate syntax.

CPP (C Preprocessor)

There's also languages to consider like CPP, that use this type of conditional:

#ifdef FOO

This conditional isn't built with any sort of expression.

Though, CPP also has:

The ‘#if’ directive allows you to test the value of an arithmetic expression, rather than the mere existence of one macro. Its syntax is

 #if EXPRESSION

 CONTROLLED TEXT

 #endif /* EXPRESSION */

EXPRESSION is a C expression of integer type, subject to stringent restrictions.

It can do arithmetic fine:

$ gcc -Wall -xc - << EOF && ./a.out
#include <stdio.h>
int main() {
#if 1 + 1 == 2
  puts("success");
#else
  puts("fail");
#endif
} 
EOF
success

However, it's its own syntax, different than whatever you can put anywhere else. For example, you can't put function calls there:

$ gcc -Wall -xc - << EOF
#if printf("foo")  
#endif
int main() {}
EOF 
<stdin>:1:11: error: missing binary operator before token "("

Nor strings:

$ gcc -Wall -xc - << EOF
#if "foo"
#endif
int main() {}
EOF
<stdin>:1:5: error: token ""foo"" is not valid in preprocessor expressions

Even though you can in other places where expressions are allowed.

$\endgroup$
4
$\begingroup$

Early versions of Basic only allowed relational operators like > and < in IF statements, not in expressions.

$\endgroup$
3
$\begingroup$

Going back to ancient/archaic languages: traditional COBOL has no concept of boolean and therefore it is impossible to assign an expression to any variable.

I believe FORTRAN, and RPG also may have this limitation, but I haven't seen them in more than 30 years, so they may have gained this capability since I last used them.

$\endgroup$
1
  • 3
    $\begingroup$ Original FORTRAN didn't, but 'LOGICAL' was added by FORTRAN IV in 1962, and standardized in 1966 -- that's quite a bit more than 30 years now :-) $\endgroup$ Commented Apr 28 at 0:54
2
$\begingroup$

Tcl is another example of such a language.

Tcl, the language, has no concept of expressions at all.

Tcl only has:

  • statements of which the first word must be a command/function name. Basically statements are nothing more than a function call - tcl only has function calls and nothing else.

  • rules for what is a word and what is whitespace that separate words

  • rules for substituting/interpolating function calls and variable values inside strings

That's all, it has nothing more than that.

Tcl has no if statement. Instead if is a function available in the standard library. Also, tcl does not have a for loop. Instead for is a function available in the standard library.

You can if you want implement your own if function which will replace the if from the standard library - you wouldn't want to do this though as it may break other parts of your code but you can if you want.

So the first argument to functions such as if or while is interpreted according to how the functions are defined. So if you don't like how "expressions" work in the built-in if, while, for etc. you can always implement your own expression parser in tcl and write your own if or while functions.

Tcl does have a function called expr that evaluates its arguments the way the built-in functions expect expressions to behave. So if you want to just evaluate an expression without calling an if etc. (for example if you just want to add two numbers and assign it to a variable) you can just use the expr function:

# adding numbers in tcl requires a function call:

set x [expr 1 + 2]

# In the code above we pass 3 strings to the `expr` function:
# the sting "1", the string "+" and the string "2".
# The `expr` function (not the tcl language) will evaluate them
# as an expression.
$\endgroup$
1
  • $\begingroup$ And yes, [ is a function call $\endgroup$
    – slebetman
    Commented Apr 27 at 8:52
0
$\begingroup$

Exim string expansion works this way, conditions are a different expression space that does not overlap with other expressions, can only be evaluated in places that expect conditions ie: ${if and $filter operations. And thus cannot be stored in, or used directly from variables.

$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .