Project

General

Profile

Feature #15921

R-assign (rightward-assignment) operator

Added by nobu (Nobuyoshi Nakada) over 1 year ago. Updated 6 months ago.

Status:
Closed
Priority:
Normal
Target version:
-
[ruby-core:93126]

Description

From https://bugs.ruby-lang.org/issues/15799#change-78465, proposal of the rightward-assignment operator by =>.

$ ./ruby -v -e '(1..).lazy.map {|x| x*2} => x' -e 'p x.first(10)'
ruby 2.7.0dev (2019-06-12T06:32:32Z feature/rassgn-assoc c928f06b79) [x86_64-darwin18]
last_commit=Rightward-assign by ASSOC
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

https://github.com/nobu/ruby/tree/feature/rassgn-assoc


Related issues

Related to Ruby master - Feature #15799: pipeline operatorClosedActions
Related to Ruby master - Misc #16802: Prefer use of RHS assigment in documentationClosedioquatix (Samuel Williams)Actions
#1

Updated by nobu (Nobuyoshi Nakada) over 1 year ago

Updated by Hanmac (Hans Mackowiak) over 1 year ago

where does the rightward assign works and where it is blocked? y => x might be treated as Hash Parameter

like m y => x is this m(y) => x or still m({y => x})

Updated by nobu (Nobuyoshi Nakada) over 1 year ago

This has lower precedence, so the latter.

Updated by ioquatix (Samuel Williams) over 1 year ago

There are two areas where I think this is a great addition:

x = if foo
    bar
else
    baz
end

if foo
    bar
else
    baz
end => x

I prefer the latter, because it avoids messing with the indentation/readability of the if expression.

Additionally, sometimes I find using irb I have made very large expression. In terminal, going to start of line isn't always obvious/easy. So, I wish to save expression, usually I just press enter and then write x = _ to save last result. But when I go back in history to execute statement again, I must make same "hack". So, I wish I can just write:

very long query to get list of users => users

That way I don't need to think so hard or go back to start of statement. It might also be nice in middle of expressions, e.g.

Users.where(active: true) => active_users.where(type: "admin") => admin_users

I don't know if such usage is possible or anticipated, I just wanted to show some ideas - for long expressions sometimes I want to check the middle of the expression.

Updated by ioquatix (Samuel Williams) over 1 year ago

If it's not clear, previous statement is evaluated like:

(Users.where(active: true) => active_users).where(type: "admin") => admin_users

Updated by nobu (Nobuyoshi Nakada) over 1 year ago

ioquatix (Samuel Williams) wrote:

If it's not clear, previous statement is evaluated like:

(Users.where(active: true) => active_users).where(type: "admin") => admin_users

It can't be higher precedence than ., or it will conflict with other syntaxes too much.
Rather it should be interpreted like as:

admin_users = (active_users.where(type: "admin") = Users.where(active: true))

Though it is a syntax error at the parenthesis after where currently.

Updated by ko1 (Koichi Sasada) about 1 year ago

  • Assignee set to matz (Yukihiro Matsumoto)

Updated by nobu (Nobuyoshi Nakada) about 1 year ago

nobu (Nobuyoshi Nakada) wrote:

ioquatix (Samuel Williams) wrote:

If it's not clear, previous statement is evaluated like:

(Users.where(active: true) => active_users).where(type: "admin") => admin_users

It can't be higher precedence than ., or it will conflict with other syntaxes too much.

You may be able to use |> here.

Users.where(active: true) => active_users |> where(type: "admin") => admin_users

Updated by sawa (Tsuyoshi Sawada) 8 months ago

I think => is okay, but in case we want to use a keyword (ordinary word) for this feature, I think as would be good. as in SQL is similar to rightward assignment.

(1..).lazy.map {|x| x*2} as x
p x.first(10)

Updated by matz (Yukihiro Matsumoto) 7 months ago

Accepted. I choose =>. Some confusing cases should be warned (by the compiler or a cop) e.g.

m((a=>b))
m (a=>b)

Matz.

#11

Updated by nobu (Nobuyoshi Nakada) 7 months ago

  • Status changed from Open to Closed

Applied in changeset git|1b2d351b216661e03d497dfdce216e0d51474664.


Rightward-assign by ASSOC

[Feature #15921]

Updated by jeremyevans0 (Jeremy Evans) 7 months ago

Cases where => is used outside hashes, arrays, and method call arguments currently are syntax errors. This changes things so that they are not syntax errors, but mean something quite different. I foresee this as a source of future bugs and confusion, especially to new Ruby programmers. Here are some other examples that look similar but act very different:

  • a=>b vs [a=>b]
  • {(a => b) => c} vs {p(a => b) => c}
  • ->{a=>b}.call vs {a=>b}.

Are we going to mark this as experimental (similar to the pipeline operator was when it was introduced last year), or are we sure this syntax will be supported in Ruby 3?

Updated by Eregon (Benoit Daloze) 7 months ago

expr in var already allows rightward assignment:

$ ruby -e '(1..).lazy.map {|x| x*2} in x; p x.first(10)'

Why adding another operator for the same thing?

Updated by Dan0042 (Daniel DeLorme) 6 months ago

Until now I thought => made perfect sense, given that it's already used in rescue, but Jeremy's counterpoint examples are very convincing. There's a high potential for confusion and bugs. Even matz says confusing cases should be warned.

I think => feels natural only because it's preceded by the rescue keyword. That makes it easy to tell apart from other => syntax. This is similar to how pattern matching has syntax very similar to hash literals but you can tell them apart because the pattern is preceded by the in keyword.

So I'd like to tentatively propose =|> for rightward assignment. Full proposal at #16794

Updated by Dan0042 (Daniel DeLorme) 6 months ago

I'd like to hear some clarifications on the expected behavior of rightward assignment.

assignment at both ends? x = expr => y
multiple assignment? expr => x => y
splatted assignment? *expr => x,y
auto-splatted assignment? expr => x,y
with method call chained on next line?
expr => x
.foo

Updated by zverok (Victor Shepelev) 6 months ago

In current head, it is so:

x1 = 5 + 3 => y1
p [x1, y1] # [8, 8]

5 + 3 => x2 => y2
p [x2, y2] # [8, 8]

*[1, 2, 3] => x3, y3
#          ^ syntax error, unexpected =>, expecting '.' or &. or :: or '['

[1, 2, 3] => x4, y4

p [x4, y4] # => [1, 2]

5 + 3 => x5 
  .then(&method(:puts))
# syntax error, unexpected '\n', expecting '.' or &. or :: or '['

That mostly makes sense to me (except for case 3, which seems to be "intuitively possible").

Updated by Dan0042 (Daniel DeLorme) 6 months ago

Ah right, trying the master branch is the fastest way to get answers, duh. But this brings me to this little surprise:

5 + 3 => x      #=> 8
5 + 3 => x.to_s #=> undefined method `to_s=' for 8:Integer
5 + 3 => x
.to_s           #=> undefined method `to_s=' for 8:Integer

So these last 2 expressions are equivalent to x.to_s = 5 + 3 which tries to call the to_s= attribute writer. That makes sense but at the same time it really caught me by surprise! It turns out you can also do this with rescue, i.e. rescue => obj.attr. You learn something new everything day.

#18

Updated by Eregon (Benoit Daloze) 6 months ago

  • Related to Misc #16802: Prefer use of RHS assigment in documentation added

Updated by Eregon (Benoit Daloze) 6 months ago

I think it would be good to make it a habit to justify any syntax change with some motivation.
For instance, I would suggest making sure the point is clear in the ticket before accepting a syntax change.

I don't see much examples here, where normal assignment wouldn't work just fine and be as or more readable (fib(10) => x in the NEWS seems no better than x = fib(10)).
In fact the only examples seem to be from ioquatix (Samuel Williams), and the second one only applies to REPL.
For the if case I would usually just assign in both branches, which generalizes nicely if there is more than one variable to assign from the if.

It seems I'm not alone wonder the usefulness of this change:
https://twitter.com/devoncestes/status/1256222228431228933
https://twitter.com/eregontp/status/1256544073554563072

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

For me, the main use case is at the end of method chains. Instead of e.g.

Word = Struct.new(:text, :count)
words = $stdin.read
        .scan(/[-\w']+/)
        .group_by(&:downcase)
        .collect { |key, value| Word.new(key, value.count) }
        .sort_by { |w| [-w.text.length, w.text] }

we can now write

$stdin.read
      .scan(/[-\w']+/)
      .group_by(&:downcase)
      .collect { |key, value| Word.new(key, value.count) }
      .sort_by { |w| [-w.text.length, w.text] } => words

where the order of the code pieces aligns with the order of what's happening, and there's no need to go back with your eyes.

(This is an example of a problem given to students, they have to count words in a text and output them in a specific order. It's from a C programming class where I show them after the submission deadline that it's much easier and shorter in Ruby.)

I'm not sure if having the assignment on the following line is possible (see below), but I think it should be. I'll submit a feature request if it is not yet possible.

$stdin.read
      .scan(/[-\w']+/)
      .group_by(&:downcase)
      .collect { |key, value| Word.new(key, value.count) }
      .sort_by { |w| [-w.text.length, w.text] }
=> words

Updated by nobu (Nobuyoshi Nakada) 6 months ago

duerst (Martin Dürst) wrote in #note-20:

I'm not sure if having the assignment on the following line is possible (see below), but I think it should be. I'll submit a feature request if it is not yet possible.

It was removed at https://github.com/ruby/ruby/commit/478135f480b4580d068d236f491b2a32048bc193.

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

At https://bugs.ruby-lang.org/issues/16775#change-85434, Eregon (Benoit Daloze) wrote:

  • Could matz and other committers clarify the motivation to introduce this? There is no pipeline operator currently so it seems of limited usage.
  • Changing syntax often divides the community (e.g., https://twitter.com/devoncestes/status/1256222228431228933), should we be more careful when introducing new syntax? For instance, on syntax issues matz considers to merge, he could state his intention, tweet about the proposal, let it be for at least a month and then decide (merge or not). A blog post by ruby core would be another way to trigger feedback before merging. Often it feels like a decision "out of the blue" with no clear motivation. Discussion after merging feels suboptimal because people realize they have very little chance to change anything and so just love or hate it. Being in master doesn't help much because most people won't try it, yet they can share their opinion based on code snippets using the new syntax. <<<<

Developers Meeting issues are not suited for extensive discussion, so I have copied these comments here. I agree quite a bit with the suggestion in the second part, but I'm very puzzled with the first part.

The "pipeline operator" is not the only syntactic construct where data flow goes from left to right. Indeed, Ruby's most basic construct, method invocation, leads to a data flow from left to right in the form of method chains. If an R-assign operator is suitable after some pipeline operator(s), it sure should be suitable after a method chain.

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

I do not have any particularly strong opinion either way, but I would like to point out
that the example given by duerst made the most sense to me personally, from all the
examples given above as to the potential usefulness. :)

I would actually go as far and suggest to include that example, or a similar one, for
the rightward-assignment in the official documentation (the content in this issue will
otherwise be forgotten eventually, and when people may stumble upon that feature,
they may ask "What is this feature used for?").

To be clear, I refer to this specific example by Martin there:

$stdin.read
      .scan(/[-\w']+/)
      .group_by(&:downcase)
      .collect { |key, value| Word.new(key, value.count) }
      .sort_by { |w| [-w.text.length, w.text] } => words

where the order of the code pieces aligns with the order of what's happening,
and **there's no need to go back with your eyes** .

In particular the last explanation made sense to me, and I write this in the sense that
I will most likely not use that specific feature (I don't think I need it). I think
duerst's explanation was really good - perhaps there are more use cases, but the
explanation given there made by far the most sense to me personally.

Updated by osyo (manga osyo) 6 months ago

hi.
I have summarized the expected behavior and the actual behavior with right assignment.

see: https://gist.github.com/osyo-manga/ef1db68fcb62a6fce7dace0f655c0b17

I think there is another issue of priority, apart from the fact that => is defined as Hash.
This is a problem even if the => symbol changes.

# Example ambiguous call example
# assume `=>` is another symbol(`>>>`)
func 42 >>> value      # value = func(42)  or  func(value = 42)

func a, b >>> value    # value = func(a, b)  or  func(a, value = b)

a || b >>> value       # a || (value = b)  or  value = (a ||b)

cond ? a : b >>> value # cond ? a : (value = b)  or  value = (cond ? a : b)

func cond ? a : b >>> value   # value = func(cond ? a : b)  or  func(value = cond ? a : b)

I think it is necessary to clarify priorities first.

For example,

  • other operator method call > = = right assignment > , || && other syntax(if, while...)
# method call takes precedence
func 42 >>> value      # value = func(42)

# method call takes precedence
func a, b >>> result   # value func(a, b)

# right assignment takes precedence
a || b >>> value       # a || (value = b)

# ?: operator takes precedence
cond ? a : b >>> result   # value = (cond ? a : b)

# method call and ?: operator takes precedence
func cond ? a : b >>> value   # value = func(cond ? a : b)

# right assignment takes precedence
for i in [1, 2, 3] >>> value; end  # for i in value = [1, 2, 3]; end

# right assignment takes precedence
42 >>> value if cond   # value = 42 if cond

Thank you :)

Also available in: Atom PDF