crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.38k stars 1.62k forks source link

Combined assignments in short block syntax #11649

Open HertzDevil opened 2 years ago

HertzDevil commented 2 years ago

I think certain short blocks that involve combined assignments should be accepted:

&.x.+= 1
&.[2].&*= 3
&.[4][5, 6].||= 7

The above short blocks should be equivalent to:

{ |__arg0| __arg0.x += 1 }
{ |__arg0| __arg0[2] &*= 3 }
{ |__arg0| __arg0[4][5, 6] ||= 7 }

They in turn are normalized like any other combined assignments:

{ |__arg0| __arg0.x=(__arg0.x.+(1)) }
{ |__arg0| __arg0.[]=(2, __arg0.[](2).&*(3)) }
{ |__arg0| temp = __arg0.[](4); temp.[]?(5, 6) || temp.[]=(5, 6, 7) }

This requires that the receiver of the combined assignment within the short block must be a call and a valid assignment target that can expand to a setter call. The following would be disallowed:

&.+= 1        # receiver is a variable (no-op variable assignment)
&.||= 1       # receiver is a variable (no-op variable assignment)
&.x.= 1       # `=` is not a combined assignment
&.-.+= 1      # `-+=` is not a setter (see also #11648)
&.foo(1).+= 2 # call with arguments is not an assignment target
&.foo().+= 2  # call with parentheses is not an assignment target
&.foo!.+= 1   # call ending with `?` or `!` is not an assignment target

Also multiple assignments will most likely be invalid because the short block syntax is effectively left-associative:

&.+=(foo.bar).+=(1)
# equivalent to:
{ |__arg0| (__arg0 += foo.bar) += 1 } # combined assignment is not an assignment target
# but _not_ equivalent to:
{ |__arg0| __arg0 += (foo.bar += 1) }
naqvis commented 2 years ago

IMHO this makes code look more cryptic. Just to gain fewer keystrokes at the cost of cryptic code should be avoided.

HertzDevil commented 2 years ago

In what ways are these short blocks more cryptic than existing short blocks that don't have combined assignments (e.g. all .+= replaced with .[]=), other things being the same?

straight-shoota commented 2 years ago

I'm skeptical about actually using these for stylistic reasons, but I think it makes sense to implement for completeness.

HertzDevil commented 2 years ago

Actually, if we do this then we might as well support it for calls that aren't short blocks, for completeness:

foo.bar.+=(1) # same as `foo.bar += 1`

case [1]
when .[0].+=(1) # does not actually work, because methods cannot be chained here yet
end

The same restrictions apply to the receiver chains of these combined assignments.