9
$\begingroup$

In C++, 3 || 4 returns true. But in Python, 3 or 4 returns 3 instead. That's a logical operator that returns its operand. I don't know if there is a better term.

In dynamically typed languages, doing this is simple. But in statically typed languages, what could the operators with operands in different types reasonably return? And are there statically typed languages already doing this?

I think it has to satisfy at least these two criteria:

  • Changing one of the operand to a subclass that doesn't change relevant things should not make the result significantly different. (Better if it could potentially support indirect implicit casting, but I guess that might be too difficult.)
  • If the expression is used as a boolean value, it matches the result of the logical operator on the two operands cast to boolean values beforehand, at least if both always gives a consistent boolean value.
$\endgroup$
2
  • 1
    $\begingroup$ Python's x or y is a bit like what is_truthy(x) ? x : y would be in C(++), so that's one place to perhaps look at. e.g. en.cppreference.com/w/cpp/language/operator_other $\endgroup$
    – ilkkachu
    Commented Aug 18, 2023 at 12:00
  • 1
    $\begingroup$ Isn't there a trivial solution of requiring that both operands be the same type? $\endgroup$
    – Barmar
    Commented Aug 18, 2023 at 14:39

5 Answers 5

13
$\begingroup$

The logical operators && and || are statically typed in Typescript, where the result is a union type.

Many languages don't have union types, though. In general, the result will be a "least upper bound" (also called the join) of the two types. What exactly a "least upper bound" is will depend on what the language's type system supports, but in general terms, an upper bound is any common supertype, and "least" means it's a subtype of all other upper bounds.

For example, in Java there are no union types, but the type system does support intersection types*. The least upper bound of two classes might be the most specific common superclass, or it might be a common interface, or it might be an intersection of multiple such supertypes (the precise spec is here).

On the other hand, in Typescript the least upper bound of S and T is always equal to S | T, because the type system includes union types.

As André L F S Bacci notes, the ternary operator a ? b : c has the same issue, also requiring a least upper bound of the types of b and c. Likewise, a list literal like [a, b, c, d, e] will typically have a type like T[] or List<T>, where T is the least upper bound of the types of those expressions which make up the list. So this is a problem your compiler probably already has to solve.

*Intersection types in Java are not syntactically allowed in most contexts, but they are types nonetheless.

$\endgroup$
2
  • 1
    $\begingroup$ The relevant part is that the union type is actually representable. This is usually only the case for scripting languages because they box and runtime type everything. $\endgroup$
    – feldentm
    Commented Aug 18, 2023 at 15:20
  • 1
    $\begingroup$ @feldentm That's a fair point. But Java could have union types too, for reference types at least, without changing the value representation ─ it just wouldn't be that useful without some kind of control-flow narrowing, e.g. if(x instanceof Foo) { ... } else { ... } where x is known to be Bar in the else branch by elimination. $\endgroup$
    – kaya3
    Commented Aug 18, 2023 at 15:42
5
$\begingroup$

That's a logical operator that returns its operand. I don't know if there is a better term.

Python's or return the first true object. if any, or returns the last object. It is a true coalescing operator, of sorts.

Note that True and False are also objects in Python, so there is not really anything special or being used in boolean expressions or arbitrary objects.

But in statically typed languages, what could the operators with operands in different types reasonably return?

A common implemented object in operands ancestry, or a common interface, and at least the top type (Object, any...).

In case of multiple inheritance or multiple interfaces, explicit casting will be necessary to select a common type.

Note that is the exact same problem of ternary operators in the form: a = b ? c : d. a will have a "common" type or interface inferred from c and d.

A true coalescing operator is essentially a specialized form of the ternary operator in the form ( a ? a : b ).

$\endgroup$
0
3
$\begingroup$

Such behavior appears semantically equivalent to what functional operations on the Optional type do in several languages. I'll refer to Rust in my example (except it's called Option there).

  • There's an or operation, which takes two Options and returns the first one if it has a value, or the second otherwise.
  • There's also an and operation, which returns the first one if it has no value, or the second otherwise.

Note that those operations are only defined on Options containing the same type, and it's the user's responsibility to explicitly cast the operands in advance. It doesn't have to be the case in an arbitrary language, though. As other answers have suggested, casting could also be done to the least upper bound type, but this approach at its core doesn't depend on implicit casts at all.

A statically typed language could allow boolean && and || operators to be overloadable for types other than booleans, as long as they implement boolean coercion (like Rust's Option intuitively does, with Some(...) being True and None -- False). (Rust doesn't do it, though. Although it could have benefitted from its lazy evaluation, letting it ditch or_then and and_then). This way, to achieve your desired behavior, the user would have to wrap arguments into Options to explicitly signify their intent. The resulting semantics would be exactly as you'd want:

  • Some(3) || Some(4) would be Some(3).
  • The result, coerced to a boolean, is exactly the same as if the arguments had been coerced to boolean in advance.
  • If you were to allow implicit lifting to superclasses (not talking about Rust at all anymore, assume some Rust-Java hybrid), Some(new Parent()) || Some(new Child()) would return Option<Parent>, since the second argument would have been implicitly cast to the base class due to Option's covariance.
$\endgroup$
3
$\begingroup$

VHDLs boolean operators are overloaded functions. Rather than just a single <boolish> or <boolish> there is a family of such functions, where the appropriate one is resolved based on the input arguments. This is perticuarly significant in VHDL, because there are at least 4 commonly used booleanish types: boolean, bit, std_logic and std_logic_vector. You can do boolean logic, only if both arguments are the same type, and the function that binds as a result will return that same type. The caller must explicitly convert to a common type if the input arguments are different.

$\endgroup$
0
1
$\begingroup$

TL;DR: yes and no

We need to take a step back to understand why the question is a bit strange and why programming languages do not behave like this. I'll still provide a Tyr implementation for Integer to sketch a solution.

First of all, there is no such thing as a logical operator. There is a truth type, e.g. bool that might have operators. These operator symbols might be reused by other types. Usually, there is || and |.

The first strange thing about the question is that || is usually lazy and implemented with an if then else. Most languages allowing to overload operators treat operators like functions, i.e. the right argument will always be evaluated preventing libraries to provide this behavior.

So, the remaining question is how can a statically typed language implement something like 3 or 4 in Python.

Step one, we do it just for Integer[_].

public def || (x : Block[Integer[Size]]) : Integer[Size]
     <: operator.precedence[30], operator.rightAssociative =
if this.toBool() this else x.eval()

I'll assume that there is a toBool function; it could be 0 == this or similar. It does not matter. What matters here is the Block type and eval() call. Because this is what causes evaluation to happen in the correct branch of the CFG.

With this pattern, you can define your operator for every type where you consider this behavior reasonable. My personal taste is that it is never a good idea. Especially if you provide an implicit conversion to bool.

If you are looking for the eager evaluation semantic, you could simply define it as

public def | (x : Integer[Size]) : Integer[Size]
     <: operator.precedence[50], operator.rightAssociative =
if this.toBool() this else x

Step two, extension to arbitrary types.

Here, we have several options that all come with downsides.

One option would be to add an implicit conversion from any type to bool. This would result in C++'s solution if there wouldn't be an overload that results in a value.

We could define the operator in any as any || any : any. This would require any to have a physical representation, which it usually does not have. Also, it would cause ambiguities with actual logical operations.

We could try to define an operator ∀T,U. T || U : union(T,U). This, again, would require that union(T,U) is representable, which it is not in most languages. E.g. 0 || "hello" would be either a pointer to string literal or a 32bit integer. To fix this, we'd need some sort of constraint requiring that T and U share the same physical representation. Even if there were a language that could express this relation, you'd still also need inference for all the types and means to provide sane error messages if this constraint is not fulfilled. My experience is that people who build such languages simply do not like implicit conversions from anything to bool. Hence, they won't have the operator you are looking for.

$\endgroup$

You must log in to answer this question.

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