Project

General

Profile

Feature #14724

chains of inequalities

Added by gotoken (Kentaro Goto) 10 months ago. Updated 9 months ago.

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

Description

In mathematics, chain of inequations is a shorthand notation for the conjunction of several inequations involving common expressions.

For example, a < b <= c for a < b && b <= c

Chain notation makes code clearer, in particular, long common expression cases will be so. E.g.,

cur.to_i - 2 <= timeval.tv_sec <= cur.to_i

is easier to read than

cur.to_i - 2 <= timeval.tv_sec && timeval.tv_sec <= cur.to_i

because in the latter case we have to verify whether timeval.tv_sec is common or not by eyes.

Current syntax allows but redefining builtin methods are considered not practical. So here I request as a new syntax for the chains.

Use cases (applicable conjunctions)

lib/matrix.rb:

    unless 0 <= column && column < column_count

lib/time.rb documents:

    # block.  For example:
    #
    #     Time.parse(...) {|y| 0 <= y && y < 100 ? (y >= 69 ? y + 1900 : y + 2000) : y}

spec/ruby/optional/capi/bignum_spec.rb:

  raise "Bignum#coerce returned Fixnum" if fixnum_min <= n && n <= fixnum_max

test/fiddle/test_import.rb:

        assert(cur.to_i - 2 <= timeval.tv_sec && timeval.tv_sec <= cur.to_i)

tool/jisx0208.rb:

        unless 0x8140 <= sjis && sjis <= 0xFCFC

History

Updated by gotoken (Kentaro Goto) 10 months ago

That works fine for me! Thanks for your patch. That works for not only numerics but also for hashes or classes.
And test patterns look enough. I hope to merge.

Updated by duerst (Martin Dürst) 10 months ago

  • Assignee set to matz (Yukihiro Matsumoto)

It would be nice to have this in Ruby. I have assigned it to Matz, because it's a syntax change that he should check and (hopefully) approve.

Just for documentation and NEWS, it's "(in)equalities", not "inequations".

Updated by duerst (Martin Dürst) 10 months ago

  • Subject changed from chains of inequations to chains of inequalities

Mentioned at #14698 (DevelopersMeeting20180517Japan). Fixed subject.

Updated by graywolf (Gray Wolf) 10 months ago

Just pointing out that this would be a breaking change since

a < b == c

is already valid syntax meaning something else. But it's probably not frequent code to write.

Updated by shan (Shannon Skipper) 10 months ago

I've often seen new Rubyists try this syntax intuitively. I think this is a really nice change. +1

Updated by baweaver (Brandon Weaver) 10 months ago

If it works for classes you might have just allowed for some fun stuff like this:

class Proc
  def >=(o)
    -> *as { o[self[*as]] }
  end
end

a2 = -> a { a+2 }
m2 = -> a { a*2 }

[1, 2, 3].map(&
  a2 >=
  m2 >=
  proc { |v| v * 20 }
)

Though with compose it'd allow >> already. Nifty thing is to_proc as & evaluates after infix operators, allowing this type of thing.

+1 for beginner intuitive use, expert brevity, and silly fp hacker shenanigans

Updated by phluid61 (Matthew Kerwin) 10 months ago

shan (Shannon Skipper) wrote:

I've often seen new Rubyists try this syntax intuitively. I think this is a really nice change. +1

-1

I've seen beginners write all sorts of things. This makes it harder for experienced programmers.

Ruby has many innovations, many of which are great, but its fundamental operator chaining has always been pretty much the same as most other programming languages.

Updated by shan (Shannon Skipper) 10 months ago

phluid61 (Matthew Kerwin) wrote:

shan (Shannon Skipper) wrote:

I've often seen new Rubyists try this syntax intuitively. I think this is a really nice change. +1

-1

I've seen beginners write all sorts of things. This makes it harder for experienced programmers.

Ruby has many innovations, many of which are great, but its fundamental operator chaining has always been pretty much the same as most other programming languages.

How does this patch make things harder for experienced programmers? Reading the new syntax that others have written? I don't see that as much of a hurdle (TIMTOWTDI), but is that what you mean about making it harder? Or are you concerned about breaking existing code? If the latter, have any examples?

If this patch is merged, I'd expect many experienced Rubyists to eventually adopt this syntax with chained comparison for the sake of readable code.

Updated by phluid61 (Matthew Kerwin) 10 months ago

shan (Shannon Skipper) wrote:

How does this patch make things harder for experienced programmers? Reading the new syntax that others have written? I don't see that as much of a hurdle (TIMTOWTDI), but is that what you mean about making it harder?

Yes, a fair chunk of my professional life is taken up with reading, analysing, debugging, and refactoring others' code. The more weird ways people have of doing things, the harder it can be to understand what they're doing, or what they intended to do.

Or are you concerned about breaking existing code? If the latter, have any examples?

No, not breaking existing code; but removing a syntax error introduces the chance of certain types of mistakes being missed. Mangle a merge, or bump backspace, and what would have been a syntax error becomes unexpected behaviour somewhere else down the line.

For example: x1 <= x2 && y1 < y2 vs x1 <= x2 & y1 < y2

If this patch is merged, I'd expect many experienced Rubyists to eventually adopt this syntax with chained comparison for the sake of readable code.

I'm on the fence about "readbility" here. Brevity doesn't always lead to readability (see: lambda arrow syntax 🤢). One of the projects I maintain is a large Perl codebase, so I have a Pavlovian negative reaction to suggestions for "brevity" or magical symbology. Maybe if I'd come from Python I wouldn't feel so bad about it.

If I wasn't on my phone earlier I would have probably written "-½", because I actually do like the fact that the middle terms are only evaluated once.

Updated by graywolf (Gray Wolf) 10 months ago

I don't think this can be reasonably implemented. Implementing it as it is now ("chains of inequalities") would be way too confusing. I mean, why should a < b < c evaluate as (a < b) && (b < c) while a < b || c evaluate as (a < b) || c? And implementing it even for other operators would silently break existing code which I don't think is justifiable in this case.

After all Matz will decide, but imho this should not be implemented.

Updated by graywolf (Gray Wolf) 10 months ago

phluid61 (Matthew Kerwin) wrote:

No, not breaking existing code; but removing a syntax error introduces the chance of certain types of mistakes being missed. Mangle a merge, or bump backspace, and what would have been a syntax error becomes unexpected behaviour somewhere else down the line.

For example: x1 <= x2 && y1 < y2 vs x1 <= x2 & y1 < y2

To nitpick a little here, it's NoMethodError, not SyntaxError. And I'm still surprised that FalseClass doesn't have < operator. Never knew before reading this discussion. Seems weird.

Updated by Hanmac (Hans Mackowiak) 10 months ago

if x <= y <=z is expanded to (x <= y) && (y <= z)

then if (x <= y) would return something else than boolean, we are doomed

PS:
also (x..z) === y is a current solution

Updated by gotoken (Kentaro Goto) 10 months ago

duerst (Martin Dürst) wrote:

Mentioned at #14698 (DevelopersMeeting20180517Japan). Fixed subject.

Thanks for cheking it out and giving correct subject. Yes, the inequation is a declarative one.

Updated by gotoken (Kentaro Goto) 10 months ago

graywolf (Gray Wolf) wrote:

Just pointing out that this would be a breaking change since

a < b == c

is already valid syntax meaning something else. But it's probably not frequent code to write.

Thanks for pointing out. In this request, I intended only four inequality operators <, >, <= and >=. Sorry for confusing. I don't aim incompatibility now.

Updated by gotoken (Kentaro Goto) 10 months ago

I heard non-comparison use of #< in shell.rb http://ruby-doc.org/stdlib-2.5.0/libdoc/shell/rdoc/Shell.html#class-Shell-label-Pipe+-2Fetc-2Fprintcap+into+a+file
that uses < and > for redirection. For example a file copy from src.txt to dst.txt can be written as:

sh = Shell.new
sh.cat < "src.txt" > "dst.txt"

This is an imcompatible case because Shell::Cat#< returns non-boolean Shell::Cat to processes next #> whereas chain expects boolean only.

Some options:

  1. if a non-boolean is returned, treats the rest of chain in the compatible manner.
  2. detects only same direction operators, e.g., a < b <= c is a chain, but a < b >= c is not a chain.
  3. accepts incompatibility.

I hope 1 if it is possible.

Updated by shevegen (Robert A. Heiler) 10 months ago

And implementing it even for other operators would silently break
existing code which I don't think is justifiable in this case.

After all Matz will decide, but imho this should not be implemented.

I do not know how matz will decide; I personally am neutral here. I
really have no problem either way.

I think it may be discussed at the next meeting; Martin Dürst already
added it (I think).

As for incompatibilities, in principle this could fit to ruby 3.x but
I think matz also said somewhere that he may not necessarily want to
add (too many?) incompatible changes from 2.x to 3.x. I am not sure
if I quote him correctly, but this is what I have gathered. So 2.x to
3.x will not be like 1.8.x to 1.9.x, if I understood it correctly.

On a side note, if we ignore the specific versions, we could also ask
the question differently, for any feature: :)

  • if matz would create a new language, like ruby, but give it a different name, would this or that feature be part of the language? I really have no idea in regards to the feature here, but I think depending on that answer, this may or may not have some merit (e. g. if we ignore the situation of backwards incompatibility, since that situation often means that a change may not happen even if it could have happened if a ruby-like language were created anew today. Not sure if I managed to get across what I was trying to say ... but again, I am completely neutral on this, not having a pro or con opinion. I am more looking at the mjit-related changes. :D)

Updated by Eregon (Benoit Daloze) 10 months ago

gotoken (Kentaro Goto) wrote:

  1. if a non-boolean is returned, treats the rest of chain in the compatible manner.
  2. detects only same direction operators, e.g., a < b <= c is a chain, but a < b >= c is not a chain.
  3. accepts incompatibility.

I hope 1 if it is possible.

I think (1) is too magic and not consistent.
What if < could return legitimately a boolean or something else? Then a < b <= c would become unpredictable.
Every conversion to boolean in Ruby follows !(v.equal? false || v.equal? nil),
it would be bad to break that by e.g. treating nil completely differently than false in such a case.

(2) makes more sense to me, although it will likely still be occasionally surprising (but hopefully rarely).

(3) seems fine too as I would guess extremely few libraries use comparison operators like that.
At least then everything is consistent, and we only need to patch the little-used Shell class.

Updated by gotoken (Kentaro Goto) 10 months ago

A Eregon (Benoit Daloze) wrote:

  1. if a non-boolean is returned, treats the rest of chain in the compatible manner.
  2. detects only same direction operators, e.g., a < b <= c is a chain, but a < b >= c is not a chain.
  3. accepts incompatibility.

I hope 1 if it is possible.

I think (1) is too magic and not consistent.
What if < could return legitimately a boolean or something else? Then a < b <= c would become unpredictable.
Every conversion to boolean in Ruby follows !(v.equal? false || v.equal? nil),
it would be bad to break that by e.g. treating nil completely differently than false in such a case.

I don't think (1) is too magic but I agree (1) is magic. Probably there are only two kinds of <s in this world: returns boolean and returns non-boolean. Potentially returning both depending on some conditions is not illegal but I could not imagine such use cases. About nil I think it is better to treat nil as same as false because nil.< is undefined and (a < b) < c is not obviously intended if a < b is nil.

Nevertheless, I would change my thought if this unpredictability causes performance decrement of optimizations seriously.

(2) makes more sense to me, although it will likely still be occasionally surprising (but hopefully rarely).

Maybe so. A weakness of this option is that there some strange incompatible cases may use a < b < c in their special manner. They will not be saved by this option.

(3) seems fine too as I would guess extremely few libraries use comparison operators like that.
At least then everything is consistent, and we only need to patch the little-used Shell class.

Thank you for positive comment. I thought so too but I found another knid of incompatible library today. That is Rucas, a computer algebra system (looks prototype level) https://github.com/jdleesmiller/rucas

This interesting project seems stopped long years however Rucas can represent chained inequalities. One can with Rucas (and facets 2.8.1) do:

require "rucas"
include Rucas::Symbolic
var :a
var :b
var :c

formula = a < b < c

p formula 
#=> #<struct Rucas::LTExpr op=:<, lhs=#<struct Rucas::LTExpr op=:<, lhs=#<struct Rucas::VarExpr name=:a>, rhs=#<struct Rucas::VarExpr name=:b>>, rhs=#<struct Rucas::VarExpr name=:c>>

puts formula
#=> a < b < c

I often have scenes that I would like to write a < b < c in practical code and wrote this request but I don't want to put obstacle to such DSL if it is possible.

Updated by graywolf (Gray Wolf) 10 months ago

After reading comments above I believe even more strongly that feature like this does not belong in language which allows you to redefine operators. It's one thing with python's __lt__ but when we can actually redefine < itself to return whatever this will become too confusing for anyone trying to do so. That small saving in number of characters written does not justify the added complexity.

If it is actually implemented (I don't think it should), I think that:

  1. evaluating a < b < c to (a < b) && (b < c) is incorrect since the double access to b is not really obvious. It would create non-obvious performance issues / bugs in both a.get_from_db_takes_a_long_time() < b.get_from_db_takes_a_long_time() < c.get_from_db_takes_a_long_time() and a.get_and_increase() < b.get_and_increase() < c.get_and_increase(). It would be better to touch each term just once.
  2. It really should not be limited to "chains of inequalities", it should be implemented for all operators. Just changing a < b < c to a < b == c should not break things (again, it's not really obvious that it will stop working).

After all, if we are copying this python feature, why not copy it whole.

Updated by shevegen (Robert A. Heiler) 9 months ago

Here is the link to the discussion at the developer meeting or rather the
log written:

https://docs.google.com/document/d/e/2PACX-1vR2LdBE87iEcEsVuUUr0G2L6LxSPeGMg_0oeHeh0HYmX36iIa9zkWYlFHilH5D4I_RBJpQnr09yOZaE/pub

If I understood it correctly then the biggest objection is not onto the
suggested feature itself as-is, which I think nobody minds in itself, but
that code may be broken as a result.

In particular comments from nobu and koichi indicate this:


Nobu: that patch breaks existing code.
Nobu: shell.rb
Ko1: so this patch breaks DSLs that use < characters.


Usa: what about #< to return self instead?
Akr: ruby up to 0.51 behave like that.

The last statement by Akr is actually interesting to me - I have not used
ruby 0.51. Might be interesting to see how that was. :)

I personally am not affected either way (my ruby code is so simple in
general that I do not need to do too complicated checks), but for those
who consider the feature suggestion useful, perhaps you may have an
idea how to approach this issue. If not for ruby 3.x, perhaps for
ruby 4.x. :D (Could be archived and re-activated in this case.)

Updated by Student (Nathan Zook) 9 months ago

As a mathematician, this suggestion really has my attention.

First, I almost never write a <= b && b < c. What I do write is some form of (a...c) === b. There are two reasons for this. In the first case, b is being repeated, and we want to avoid this if we can do so without compromising clarity. Secondly, and more importantly, because b is being evaluated twice, the expression might evaluate "inconsistently" if b has a time dependency. Note: one of the examples given DOES have a time dependency, although it is between a and c. This is a real problem which regularly crops up in production code.

Second, I must object strenuously to the suggestion that a < b < c become valid but a < b > c < d < e < f > g not. That would be a serious inconsistency from the standpoint of someone who makes regular use of such notation, and also a violation of the description of "chaining" operators.

Third, the suggested implementation (to treat a < b < c as (a < b) && (b < c)) is simply wrong as it creates an invisible time dependency on b that does not exist in the mind of the average programmer. If this suggestion is to be implemented, it needs to be implemented as follows:
1) evaluate "a", and store value in x1.
2) evaluate "b", and store value in x2.
3) return false unless x1 < x2.
4) return x2 < c.

Fourth, the previously-mentioned ambiguity of a < b == c created by chaining is a significant problem. While == does not mean "<= and >=", that is a subtly that is going to catch people up. While I agree that the only solution, unless we are just trying to break user code, is to exclude == from such changes, it is going to surprise people who start using a < b < c-type constructs.

Fifth, Ruby allows us to define operators on classes, and even to overwrite them on core classes. The suggested change has the potential to create nasty breaks in existing code. If implemented, I would urge that it be delayed until Ruby 3, but announced immediately so that the community is able to prepare for it.

Also, Shell might be used more in private code than in published code. I'm pretty sure I've used it professionally.

On balance, I don't really like this idea. AFAIK, we only have one tertiary operator right now, ?:, and nothing higher. The only consistent implementation would create an n-ary operator. While this would certainly be "cool", the common use cases are in fact adequately covered with range operators.

Also available in: Atom PDF