[Pre-RFC] Boolean operators that don't return bool


#1

Relevant discussion on the Rust subreddit: http://www.reddit.com/r/rust/comments/2xu8rn/boolean_operators_to_not_return_bools/

TL;DR: I’d like to see the boolean operators currently defined by Eq and Ord be made available to return non-booleans. I think this includes all of ==, !=, <, <=, >, >=. I would expect Eq and Ord to retain their current semantics, but I’m not sure if that is possible to do in a backward-compatible way.


In Python, the boolean operators can be overloaded to return any type. It’s more than a bit convoluted how it’s done, with __add__ and __radd__, etc to make it all work, but it makes some particularly nice-looking APIs. Take an example from usage of the SQL Alchemy ORM:

MyModel.query.filter(MyModel.my_field < 42).first()

If Python enforced that MyModel.my_field < 42 had to return a bool, this would not be possible. Of course, since Python doesn’t enforce any return types, it doesn’t, but it’s clear that this polymorphism is desired in other operators, such as +, -, etc.

The way I envision using these operators is by creating traits for each of those operators (I don’t know that forcing them to be implemented together is strictly necessary in all cases). For the sake of discussion perhaps they could be called Equal, NotEqual, Gt, Ge, Lt, Le. If I had all my druthers, I might instead rename Eq to Equiv, and use Eq and Ne to match their method names, but I suspect that’s even less possible than the rest of this proposal.

Then I’d like to see Eq and Ord simply be one possible concrete abstraction over a usage pattern for these operators. I suspect that there are other legitimate reasons for wanting to overload these operators than just an ORM, though my reasoning includes only that use-case and my desire for having operators in general work the same across the board (like std::ops).

Because these are fundamental types and operators that I’m proposing changing, this may be too little too late and too close to 1.0, but I would be remiss to not bring it up for discussion.


#2

I’m curious if there are any use cases for this that wouldn’t be satisfied with an ast! macro.

Also, I’d be concerned that this would make using these traits far more painful. For example, if something like fn foo<T: Eq>(a: T, b: T) -> bool would have to become fn foo<T: Eq<Result=bool>>(a: T, b: T) -> bool. Arithmetic traits are kind of a pain as it is because of this.


#3

Huge -1

This is a system language, you need to know what your code is actually doing. If I write ==, + or * I want to be sure that it executes the corresponding operation, and not produce some sort of marker or lazy evalution thing.

If you want to write something like MyModel.query.filter(MyModel.my_field < 42), the right Rust tool is a macro: my_model.query.filter(filter! { MyModel.my_field < 42 }).


#4

@DanielKeep: I’m not sure that there are any, because I’m not very familiar with an ast! macro (I need to find those docs).

I certainly get that the ergonomics of this kind of flexibility are annoying, but I have trouble understanding why all the other operators work this way, but not the ones that are defined by Eq and Ord. It seems to me that as much as possible operators should be implemented consistently, and this seems very inconsistent.

@tomaka: Are you then arguing that allowing the overrides we already do in std::ops is too much, because it allows that? I’m inclined to think not. Why should these operators be less overridable than the others?

I agree that it needs to be apparent what happens when you use an operator, but I think that applies equally well to any method or function call, and should be relegated to API design. While there are some libraries that bastardize some operators in Python, they are, in my opinion, used very judiciously by the popular libraries that do override them. I think that the way that SQL Alchemy overrides the == operator in Python is perfectly in line with its obvious semantics, it just allows those semantics to be captured and used elsewhere.

For many (most) types, the semantics of Eq and Ord are exactly what is needed. However, I think that it’s not always exactly what is needed, and that the current operators are surprisingly uncustomizable.


#5

-1

better fit for macros. will read the whole rfc later, but i think that this type of change is a good idea.


#6

It doesn’t exist, but it could be written. The idea is that ast!(my_field < 42) would be expanded, at compile-time, into a value that represents the expression.

The thing is that arithmetic needs parameterised output to support things like matrix multiplication. Equality is defined as producing a logical true/false result. It’s less flexible, yes. I’m personally not convinced that the flexibility is really worth it, assuming the creation of an ast! macro.


#7

I’m quite familiar with LINQ and various other attempts to create a fluent interface for queries.

You really need to have operators that return special wrapped types. Going with ast! route you’ll lose typechecking pretty quickly. Additionally, you often have to perform “strange” actions with database values - a result of “a < b” might be a database NULL.

Besides, with AST macros you’re going to lose autocomplete support in IDEs.

What would work: simply replace “<” with a special method “.lt”.

PS: macros should be abolished in general.


#8

So what you’re saying is, you could have a macro that rewrites all the a < b expressions as a.lt(b)? Sounds good to me! :stuck_out_tongue:

But seriously, typechecking is a good argument, but I still feel concerned that it would make using comparisons in generic code a massive PITA. I like definition-checked generics, but Rust is already uncomfortably close to the “too verbose to be worth it” line.

Burn the heathen!


#9

Macros

Macros for this work have a serious downside, as has already been noted: they are inherently not part of the normal syntax of the language, and thus language tools won’t understand them, only the compiler. This restriction is worked around for built-in macros, but it can’t be eliminated in the general case. The restriction means that errors in the the syntax won’t be normal syntax errors, they will be errors resolving the macro.

While that is impractical in general, I think that considering them a very last resort is wise. The language, without macros, should have sufficient expressiveness if at all possible. Macros are a powerful tool, but we must be careful to use it wisely. I’m not satisfied that this is a wise use of macros.

Methods versus operators

Methods are the next best option if operators are not going to be able to happen. I don’t think macros are as good an option. I think that methods aren’t as clear, though, as using operators. Consider this example, which I would like to express a described calculation, including the associativity of the operands:

(MyModel.my_field < 13) == MyModel.my_field.lt(13)
(13 > MyModel.my_field) == 13.gt(MyModel.my_field)

The translations are straightforward, and not too ugly, but I think that operators express the idea in a more digestible, obvious way.

Operators more obviously vary based on their right operands. When I call a method, it usually makes sense to think of it as value based on doing an operation to the LHS. With operators, however, I think it’s more typical to understand that an operator’s result is based on the combination of the LHS and the RHS.


#10

Just be Lispy and write gt(13, MyModel.my_field). ;p

Edit to be more constructive: One big problem with C+±style operator overloading is that there is only a finite number of operators, and the resulting syntactic abuse (e.g. << on streams) seriously hurts readability and sanity. Yet Haskell’s solution of allowing any special character sequence to become an operator is… well, its own type of nasty. Perhaps a compromise would be borrowing Haskell’s ability to say “foo `f` bar” for “f foo bar”.

Edit to be less constructive: Though honestly, macros are not that evil and if you’re worrying about the consequences of a language tool not understanding one tiny piece of your code, the fault probably lies in the (thus far hypothetical) language tool.


#11

Custom operators seem like something we would want, but absolute integer precedence values are not optimal. @zwarich seemed to know of a better way to handle precedence, but I didn’t look into it when he first mentioned it.


#12

The problem is that a language tool cannot reasonably be expected to understand macros. It would have to know as much as the compiler to do so, and expand the macros.


#13

Language tools should know as much as the compiler, but nevertheless I expect them not to be especially helpful around macros. The point is that this should only be a small problem.