Feature #15921
closedR-assign (rightward-assignment) operator
Added by nobu (Nobuyoshi Nakada) over 5 years ago. Updated almost 4 years ago.
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]
Updated by nobu (Nobuyoshi Nakada) over 5 years ago
- Related to Feature #15799: pipeline operator added
Updated by Hanmac (Hans Mackowiak) over 5 years 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 5 years ago
This has lower precedence, so the latter.
Updated by ioquatix (Samuel Williams) over 5 years 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 5 years 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 5 years 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) over 5 years ago
- Assignee set to matz (Yukihiro Matsumoto)
Updated by nobu (Nobuyoshi Nakada) over 5 years 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) over 4 years 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) over 4 years 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.
Updated by nobu (Nobuyoshi Nakada) over 4 years ago
- Status changed from Open to Closed
Applied in changeset git|1b2d351b216661e03d497dfdce216e0d51474664.
Rightward-assign by ASSOC
[Feature #15921]
Updated by jeremyevans0 (Jeremy Evans) over 4 years 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) over 4 years 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) over 4 years 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) over 4 years 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) over 4 years 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) over 4 years 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.
Updated by Eregon (Benoit Daloze) over 4 years ago
- Related to Misc #16802: Prefer use of RHS assigment in documentation added
Updated by Eregon (Benoit Daloze) over 4 years 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) over 4 years 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) over 4 years 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) over 4 years 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) over 4 years 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) over 4 years 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 :)
Updated by dgutov (Dmitry Gutov) almost 4 years ago
This is not as bad as the "pipeline operator" (which didn't do what its name said), but still, why add this?
It's not like the new syntax makes anything possible that a simple assignment does not.
The "method chains" example is perhaps a counter-example, but having the assignment at the end of the line, after the last segment of the expression, is not so great for readability.
It also looks too much like an annotation ("here's what this expression returns") popularized by xmlfilter (e.g. http://til.justincampbell.me/annotate-ruby-code-in-vim-with-xmpfilter/), and not a real assignment.
Updated by marcandre (Marc-Andre Lafortune) almost 4 years ago
dgutov (Dmitry Gutov) wrote in #note-25:
It's not like the new syntax makes anything possible that a simple assignment does not.
It does. It is now a pattern match, not just a rightward assignment:
{x: 1, y: 2} => {x:}
# now x holds 1
Please refer to the doc on pattern matching. https://docs.ruby-lang.org/en/master/doc/syntax/pattern_matching_rdoc.html