Project

General

Profile

Feature #14951

New operator to evaluate truthy/falsy/logical equivalence

Added by danga (Dan Garubba) about 1 year ago. Updated about 1 year ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:88184]

Description

I propose adding a new operator for truthy/falsy equivalence, similar to what was proposed on https://bugs.ruby-lang.org/issues/13067, but with new syntax. The main purpose would be for writing expressions for logical equivalence (i.e., "if and only if" relationships) that only considers the truthiness the operands. Since predicate methods like File#size? and operators like =~ follow truthy semantics without returning the true and false singletons, using them in logical expressions that evaluate for logical equivalence can be error-prone without the proper return type awareness and conversions. This proposed operator would be equivalent to !!a == !!b, but I feel that a new operator would be more concise and more expressive of the concept of logical equivalence.

Attached is a prototype implementation of the operator as '=?'.


Files

teq.patch (3.47 KB) teq.patch prototype implementation danga (Dan Garubba), 07/30/2018 01:35 AM

History

Updated by nobu (Nobuyoshi Nakada) about 1 year ago

It conflicts with the existing syntax, a=?b.

Updated by shevegen (Robert A. Heiler) about 1 year ago

I am not sure if the trade off of adding a new operator is worth it in
this case, even well aside from backwards compatibility here.
But that's just my personal opinion.

Updated by matz (Yukihiro Matsumoto) about 1 year ago

Can you show me examples of concrete usage of the proposed operator?
Your explanation is a bit vague.

Matz.

Updated by danga (Dan Garubba) about 1 year ago

Sure. In my day job, I write testing code. So I've written an expression like:

raise MyError unless in_scenario_x? == actions_performed_for_scenario_x?

To express: "raise an error unless the actions are performed for scenario X if and only if we are in scenario X". However, when I implemented this, I got unexpected behavior since I was expecting boolean singleton operands for my == operator, but found that wasn't the case. I had found a non-boolean singleton operand from a Numeric#nonzero? call propagated in the underlying logic of actions_performed_for_scenario_x?, so I had to re-implement with proper type handling to ensure boolean singleton operands. If I had an operator for truthy-equivalence (not a proposal, but for this example: iff, since =? has compatibility issues) I could write something like:

raise MyError unless in_scenario_x? iff actions_performed_for_scenario_x?

So that I would only care about the truthiness of the predicate methods, and not the specific return types.

From a propositional calculus perspective, Ruby's truthy/falsy semantics seem to work just fine for the fundamental logical operations of negation, disjunction, and conjunction. But logical equivalence requires an extra degree of care for type handling when expressed using the == operator. There are workarounds, like the aforementioned !!a == !!b. But I wanted to see if the logical equivalence use case was strong enough for its own operator, or if everyone was OK living with the workarounds instead.

Updated by mame (Yusuke Endoh) about 1 year ago

How about defining a helper function for your assertions?

def assert_same_as_boolean(x, y)
  raise MyError unless !x == !y
end

assert_same_as_boolean(in_scenario_x?, actions_performed_for_scenario_x?)

It would be a good idea to avoid introducing a new operator without true necessity.

Updated by danga (Dan Garubba) about 1 year ago

Thanks. Essentially, this helper is a form of the boolean singleton normalization I apply at the application level.

I understand there should be a high threshold for introducing new operators into a language, so I wanted to see if anyone else felt as strongly about logical equivalence as I had. Ruby has looser boolean conventions for return values than other popular languages. Because of that, using == to test for logical equivalence has pitfalls that I didn't appreciate until I stumbled into the gotcha of my example. I was thinking a new operator would serve as a best practice to avoid such pitfalls and expresses the "don't care about the specific boolean type" style to the language, but the strong need probably isn't there.

Updated by sawa (Tsuyoshi Sawada) about 1 year ago

I propose to extend the exclusive or operator ^ to be defined on Object. For Integer, the method would be overwritten by the current bitwise operator, and it would not benefit from the extension, but making it available for other classes can be useful for the OP's use case (as long as it does not encounter Integer), which would then be written as follows:

raise MyError if in_scenario_x? ^ actions_performed_for_scenario_x?

Updated by jeremyevans0 (Jeremy Evans) about 1 year ago

sawa (Tsuyoshi Sawada) wrote:

I propose to extend the exclusive or operator ^ to be defined on Object. For Integer, the method would be overwritten by the current bitwise operator, and it would not benefit from the extension, but making it available for other classes can be useful for the OP's use case (as long as it does not encounter Integer), which would then be written as follows:

raise MyError if in_scenario_x? ^ actions_performed_for_scenario_x?

I don't think this is a good idea. For String#^, the most natural operation would be a bitwise OR of each byte, and for collection classes, the most natural operation would be the exclusive disjunction of the two collections (returning a collection of elements in exactly one of the two collections). Once Object#^ has been added, redefining it for more natural operations in other classes would break backwards compatibility, so I think adding it would be short-sighted.

Updated by nobu (Nobuyoshi Nakada) about 1 year ago

danga (Dan Garubba) wrote:

Sure. In my day job, I write testing code. So I've written an expression like:

raise MyError unless in_scenario_x? == actions_performed_for_scenario_x?

To express: "raise an error unless the actions are performed for scenario X if and only if we are in scenario X".

The code and the explanation differ.

If the former is correct, the latter should be:
"raise an error unless the actions are performed for scenario X and we are in scenario X, or the actions aren't performed for scenario X and we aren't in scenario X".

If the latter is correct, the former should be:

raise MyError unless in_scenario_x? && actions_performed_for_scenario_x?

Updated by danga (Dan Garubba) about 1 year ago

jeremyevans0 (Jeremy Evans) wrote:

sawa (Tsuyoshi Sawada) wrote:

I propose to extend the exclusive or operator ^ to be defined on Object. For Integer, the method would be overwritten by the current bitwise operator, and it would not benefit from the extension, but making it available for other classes can be useful for the OP's use case (as long as it does not encounter Integer), which would then be written as follows:

raise MyError if in_scenario_x? ^ actions_performed_for_scenario_x?

I don't think this is a good idea. For String#^, the most natural operation would be a bitwise OR of each byte, and for collection classes, the most natural operation would be the exclusive disjunction of the two collections (returning a collection of elements in exactly one of the two collections). Once Object#^ has been added, redefining it for more natural operations in other classes would break backwards compatibility, so I think adding it would be short-sighted.

I agree. Because of the existing mixed semantics, and predicates returning Integers, I'm wary of using ^ as a general purpose logical operator. However, !a ^ b appears to be tersest expression of truthiness-friendly logical equivalence (though your peers will probably debate you on the readability).

nobu (Nobuyoshi Nakada) wrote:

danga (Dan Garubba) wrote:

Sure. In my day job, I write testing code. So I've written an expression like:

raise MyError unless in_scenario_x? == actions_performed_for_scenario_x?

To express: "raise an error unless the actions are performed for scenario X if and only if we are in scenario X".

The code and the explanation differ.

If the former is correct, the latter should be:
"raise an error unless the actions are performed for scenario X and we are in scenario X, or the actions aren't performed for scenario X and we aren't in scenario X".

If the latter is correct, the former should be:

raise MyError unless in_scenario_x? && actions_performed_for_scenario_x?

I'm using "if and only if" as a natural language representation of 同値 per https://ja.wikipedia.org/wiki/%E5%90%8C%E5%80%A4 (i.e., what I'm referring to as "logical equivalence").

"raise an error unless the actions are performed for scenario X and we are in scenario X, or the actions aren't performed for scenario X and we aren't in scenario X".

This is appears to be have natural language expression of (a && b) || (!a && !b), which is an alternative definition of logical equivalence (per https://en.wikipedia.org/wiki/Logical_equivalence).

Updated by danga (Dan Garubba) about 1 year ago

After getting the feedback here, I no longer support my original proposal. But I would be happy if something like Object#iff? existed instead. I think logical equivalence is a legitimate use case. And alternaive expressions for logical equivalence require some degree of conversion to boolean singletons, which seems to be generally viewed as anti-idiomatic in Ruby. But if the community thinks this use case is too narrow, I'll just have to live with those truthy-safe alternative expressions, or more type discipline for boolean equality expressions.

Also available in: Atom PDF