ruby picture

RCR 307: Allow attributes to take arguments for assignment

Submitted by eric_mahurin (Sun May 15 15:34:53 UTC 2005)

Abstract

In the statement object.attribute = expression, it would be useful if the attribute could take arguments and allow this syntax: object.attribute(arguments) = expression.

Problem

Right now, the only attribute-like assignment that allows arguments is the '[]'. It would be nice if assignments to other named attributes could take arguments - index, name, flags, etc. Right now you have to just use a normal method call instead.

Proposal

In the Ruby parser, LHS (left-hand-side) for assignments is currently defined like this BNF:

 LHS : VARIABLE
     | PRIMARY `[' [ARGS] `]'
     | PRIMARY `.' IDENTIFIER

Extend the LHS syntax to allow:

     | PRIMARY `.' IDENTIFIER `(' [ARGS] `)'

and possibly even the following which would assume receiver to be self (optional):

     | IDENTIFIER `(' [ARGS] `)'

This:

 PRIMARY `.' IDENTIFIER `(' ARGS `)' = EXPR

would be equivalent to:

 PRIMARY `.' `send' `(' `:'IDENTIFIER`=' `,' ARGS `,' EXPR `)'

This new LHS could also appear in a MLHS = MRHS expression (multiple assignment).

Just like the []= method, the RHS would be the last argument given to the method.

An optional thing on this RCR would be to also allow blocks to be passed in these attribute assignments. With this, the LHS could be:

     | PRIMARY `.' IDENTIFIER [ `(' [ARGS] `)' ] BLOCK
     | PRIMARY `.' IDENTIFIER `(' [ARGS `,' ] `&'ARG `)'
     | PRIMARY `[' [ARGS] `]' BLOCK
     | PRIMARY `[' [ARGS `,'] `&'ARG `]'

And:

 PRIMARY `.' IDENTIFIER `(' ARGS `)' BLOCK = EXPR

would be equivalent to:

 PRIMARY `.' `send' `(' `:'IDENTIFIER`=' `,'  [ARGS ','] EXPR `)' BLOCK

Analysis

This just seems like it would fit well into the current syntax sugar in Ruby. It is illegal syntax now, so it shouldn't cause any incompatibilities.

Here are a few examples of potential uses:

To illustrate a little more, here is an implementation of the values_at= method mentioned above:

 class Array # or Hash
     def values_at=(*args)
         rhs = args.pop
         args.zip(rhs).each { |assign|
             self[assign[0]] = assign[1]
         }
         self
     end
 end

Here is the current usage. We need to use "send" since normal method calls can't have an '=' suffix in the method name.

 array_or_hash.send(:values_at=,*indices,values_array)

Here is the proposed usage:

 array_or_hash.values_at(*indices) = values_array

Here is an example using values_at=:

 a=(0..15).to_a
 # current usage
 a.send(:values_at=,3,9,11..15,["three","nine",["11","12"]])
 #proposed usage
 #a.values_at(3,9,11..15) = ["three","nine",["11","12"]]
 a  # [0, 1, 2, "three", 4, 5, 6, 7, 8, "nine", 10, "11","12"]

This RCR is not original. It just adds a few more options to this legacy RCR:

https://rcrchive.net/rcr/show/157

Implementation

This should be mainly a parser thing in ruby. It should fit quite naturally into the way attributes are assigned right now - just allow arguments.
ruby picture
Comments Current voting
Sounds somewhat intriguing but I am not really sure what this suggestion does, literally. Could you produce a complete example (perhaps with Ruby code that implements this, if possible)?


How does this compare to RCR157?


I didn't see RCR 157 since it was "legacy". This RCR is the same except that it is in the new format (Matz requires this) and it includes the option of also passing blocks to these assignment methods. Anybody supporting RCR 157 should support this.


What is meant by the term "attribute"? Attributes in Ruby are just methods, except they are paired with an instnce variable of the reciver. (Well, except when they're not, then they're "virtual" attributes.)

Here's my underatanding of the RCR: There are methds that take arguments and return some set of values. Since the arguments define a way to locate what values to return, these values could also be changed. But, currently, Ruby does not allow for the format

 method_name( args )=


Damn; no fsck'n preview. OK, to finish: Ruby does not allow for

 method_name( args_to_locate_values )= new_vals

So (the argument goes) some new syntax is needed.

It's interesting, though I can't think of when something like this would have helped me out. Maybe it just seems trivial to write methods that find stuff and change things.

James


I'm mildly opposed to this (but currently voting neutral) because I think that there are conceptual problems with it in that it will make at least parts of programs harder to understand. It suffers from most of the same problems as the pointers/references RCR that you introduced and I consider this something of a backdoor approach to that matter. I'm not against this particular implementation in general, but as a practical matter, there are problems in defining which methods could work this way.

I don't know what the right answer for this is, but I really don't think that this would be that useful in practice. Under what circumstances would you forsee doing such a discontinuous replacement of values as you demonstrated? I'm not saying that it isn't neat you can do this, but I just don't really see the value in it in practice. --Austin


I fail to see how this relates to pointers/references and how it has most of the same problems. This RCR is nothing more than syntax sugar. It gives to named attributes (name and name=) the ability to pass arguments just like the [] attribute. The above were just some examples I came up with quickly for the built-in classes. I've had uses for this in my own classes and ended up using standard methods names instead of attribute= type methods (because I couldn't pass additional arguments).


This is not simply syntax sugar, and you sidestepped the actual question, Eric. You said "you've had uses" -- but didn't show any.

#[] and #[]= are two separate *methods*, not attributes (which are, conventially, methods that access real or derived instance variable or instance-variable-like values). They're not simply syntax sugar (and never have been).

The problem is that you're wanting to have "baz.foo(args) = args2" which *is* a form of reference updating -- and it's ultimately meaningless. What would this mean?

  "1 3 5 7 9".scan(/\d/) = "2"

This is a logical consequence of what you've said -- and it only works if you define the argument handling that way. There's no visual difference in method definitions; there's no difference in how they would be documented by rdoc (as a consequence of the first); there's no difference between knowing that having a method on the LHS is assignable or is not assignable. Convince me that this is an absolutely vital need. It's *neat*, but fraught with entirely too many practical problems for too little positive benefit. Unless you convince me otherwise. --Austin


This is definitely syntax sugar. In the above values_at= example, if you called the method name "store_values_at" and called it with obj.store_values_at(*indices,values), you could accomplish the same thing as obj.values_at(*indices) = values. This RCR is very similar to the []= method - which is also not needed. It could have just as easily been defined/called as obj.store(index,value), but obj[index] = value offers some nice sugar. Since this is syntax sugar, ther is no way I'll ever be able to convince you of an "absolutely vital need", because there isn't. You'd be hard pressed to find any RCR that fits into that "absolutely vital need" category.

I am using the term "attribute" rather broadly. I'm using it to mean something that can appear on the LHS of an assign (or multiple assign) to generate a method call with the RHS as an argument:

hash["hello"] = "world" # same as hash.send(:[]=,"hello","world") array[5,0] = [4] # same as array.send(:[]=,5,0,[4]) io.pos = 0 # same as io.send(:pos=,0) array[5,0],io.pos = [4],0 # same as the above 2 combined

  1. proposed
io.pos(IO::SEEK_CUR) = -2 # same as io.send(:pos=,IO::SEEK_CUR,-2)


Here is another try at those "attribute" examples above:

 hash["hello"] = "world"   # same as hash.send(:[]=,"hello","world")
 array[5,0] = [4]          # same as array.send(:[]=,5,0,[4])
 io.pos = 0                # same as io.send(:pos=,0)
 array[5,0],io.pos = [4],0 # same as the above 2 combined
 #proposed
 io.pos(IO::SEEK_CUR) = -2 # same as io.send(:pos=,IO::SEEK_CUR,-2)


I just added another example usage of this RCR : protected/private '=' method. Right now you can't practically (you could use send) use these because you have to specify self as the reciever to prevent Ruby from thinking you are assigning a local variable. But, as soon as you specify a receiver (even self), you can't call a protected or private method.


Okay. I think that this is a bit more understandable now, but I still think that this will result in difficult-to-comprehend code. I still don't know that I want to see it, but it's less objectionable now. --Austin


I think that Austin at first understood this to imply that:

  x = [1, 2, 3].last(2)
  x = [3, 4]
  x # => [1, 3, 4]

Which is very unrubyish, of course. Rather the above would do what it has done forever:

  x = [1, 2, 3].last(2)
  x = [3, 4]
  x # => [3, 4]

Meaning that object assignment is still not introduced to Ruby which IMHO is a good idea.

Rather it just allows methods whose name ends with = to accept more than one parameter thus generalizing their concept. Currently:

  class Array
    def last=(new_last)
      self[-1] = new_last
    end
  end
  ary = [1, 2, 3]
  ary.last = 5
  ary # => [1, 2, 5]

But you can not yet do this:

  class Array
    def last=(*args)
      new_value = args.pop
      count = args.pop
      if count then
        self[-count .. -1] = new_value
      else
        self[-1] = new_value
      end
    end
  end

Which would for example allow this:

  ary1 = ["one", "two", "three"]
  ary2 = ["eins", "zwei", "drei"]
  ary1.last(2) = ary2.last(2)
  ary1 # => ["one", "zwei", "drei"]

I think that makes sense and while the sample is very specific I still think that there is other cases where such a syntax would make sense. Perhaps Integer#digit(index, base) = new_digit or Object#method(name) = callable. Note that all these examples refer to the standard library (because everybody is relatively familiar with it already and they are thus easier to understand), but I think that this can also make sense for simplifying the interfaces of custom libraries in some cases.

Note that I suggested the original RCR and that I am very likely biased. I also don't agree with applying blocks to assignment syntax as that would have unclear semantics in quite a few cases:

  # Who gets the block?
  obj.foo = bar { }
  obj.foo = bar do end
  # And what about this?
  obj.foo = obj.bar = qux { }
  obj.foo = obj.bar = qux do end
  # And what if I want to supply different blocks to the different assignments?

I am not sure about omitting the self on setter calls either as it seems slightly odd when compared to non-argument setters, but it makes sense when compared to the behavior of "foo" vs. "foo()" in pre-1.9.

-- Florian Gross


I don't care too much about whether assigning an "attribute" can take a block, but let me answer your questions regarding this RCR:

  # Q: Who gets the block?
  # A: bar
  obj.foo = bar { }
  obj.foo = bar do end
  # Q: And what about this?
  # A: qux
  obj.foo = obj.bar = qux { }
  obj.foo = obj.bar = qux do end
  # Q: And what if I want to supply different blocks to the different assignments?
  # A: You put the block next immediately to the right of the "attribute" or method and not just at the end:
  foo_obj.foo(*args) { foo_block } = bar_obj.bar(*args) { bar_block }

This would be equivalent to:

  foo_obj.send(:foo=,*args,bar_obj.bar(*args) { bar_block }) { foo_block }

You might want foo to have a block to handle some error condition (out-of-range index) or whatever. I don't think the syntax is ambiguous.

Basically, what this RCR is trying to do is allow the same types of things that appear on the RHS of an assign (or multiple assign) to also appear on the LHS (and have an expected meaning - RHS is the getter and LHS is the setter). Hypothetically, if all of the built-in keywords/operators actually mapped to methods, I think LHS and RHS could have identical syntax (you'd need all the corresponding keyword/operator '=' methods). But, I'm not proposing that craziness.


Ah, that is a possibility that would indeed work. So it might still be a good idea.


Eric, where you say:

  I just added another example usage of this
  RCR :protected/private '=' method. Right now you can't
  practically (you could use send) use these because you have to
  specify self as the reciever to prevent Ruby from thinking you
  are assigning a local variable. But, as soon as you specify a
  receiver (even self), you can't call a protected or private
  method. 

That's not quite right. You can use self for a "="-terminated private method:

  irb(main):001:0> class C; def x; self.y = 1; end; def y=(n);
  end; end
  => nil
  irb(main):002:0> C.new.x
  => 1

I'm also not clear on some of your answers to flgr. Wouldn't the different precedence of {} and do/end come into play in those pairs of examples of "who gets the block"? It seems like exactly the kind of case where the lower precedence of do/end makes a difference (if I'm reading it correctly).

Finally, I'm not sure why you're using 'send' so much in your examples. For example:

    hash["hello"] = "world"   # same as hash.send(
    :[]=,"hello","world")

That's true, but I usually think of it as short for:

  hash.[]=("hello", "world")

without recourse to 'send'. I think this is important because it means the change you're suggesting is not of exactly the same type as the []= sugar. In the case of your change, there is no non-meta-programming (counting send as metaprogramming) way to do the thing you mean in the first place. That doesn't make it a bad idea; it just means that the comparison with the cases that go directly from a simple method call to a sugared version may not be to the point.

David Black


You never made the y= method private in your example:

 class C; def x; self.y = 1; end; private; def y=(n); end; end

This works, but this doesn't:

 class C; def x; (self).y = 1; end; private; def y=(n); end; end

I was relying on the "Programming Ruby" book. It says this:

  • Private methods cannot be called with an explicit receiver. Because you cannot specify an object when using them, private methods can be called only in the defining class and by direct descendents within that same object.

Maybe this behavior changed from one version to the next. It does seem like some special handling is happening. I guess I should have tried it out, huh?

I was just looking at some the psuedo BNF for Ruby and it looks like both {} and do/end would work fine. '=' has a lower precedence than both.

Clearly []= vs. this RCR isn't the same. []= takes its arguments between its [] and what I describe here takes arguments with additional (). I was simply drawing an analogy because in both cases (and the standard attribute= case) there is a translation from the written syntax to how the method is eventually called. send/__send__ is just a way to call a method with an arbitrary name (with define_method it looks like any string might work). I couldn't use the normal method calling way because '=' isn't allowed in the name (except []= as you pointed out).


I didn't quite follow your last paragraph in the last comment. What name is "=" not allowed in? -- David BLack


When calling a method directly, the only time '=' is allowed in the name that I know of is []=. So, this is illegal syntax:

 a.values_at=(3,9,11..15,["three","nine",["11","12"]])

It is likely illegal to not get this method call confused with an assign (which also becomes a method call). I would have used this instead of send/__send__ if the syntax was legal.


Hm. I do not know what to think. It seems like a sort of a good idea, but certainly somewhat to understand. flgr's characterization of it as simply allowing more parameters for assignment methods clarified it. Perhaps it is merely a matter of familiarity. The syntax seems a bit off, though.

I am, still, quite concerned about the difficulty of implementation. This is a parser issue, I believe, and not an easy one at that. Since this seems to be a mostly cosmetic change, I feel that these needs may for the most part be served by keyword arguments which are already slated for addition:

ary = [1, 2, 3, 4, 5]

ary.values_at(1, 2) = [0,0] # => [0, 0, 3, 4, 5]

-vs-

ary.set 1, 2, to: [0, 0] # => [0, 0, 3, 4, 5] ary[1, 2] to: [0, 0] # => [0, 0, 3, 4, 5]

-esaynatkari


Eric --

You can do:

  a.b=(x)

but not with multiple args:

  a.b=(x,y,z)  # error

I'm not sure why, but you can always do:

  a.b= x,y,z  # or a.b = x,y,z

I wasn't sure what you meant by: a local assignment also becomes a method call.


Bah. This should be a wiki so I could save myself (some degree) of embarrassment.

 ary.set values_at: [1,0], to: [0, 0]

-es


I think the reason a.b=(x,y,z) fails is that it is trying to interpret it as LHS = RHS where LHS is a.b and RHS is the expression (x,y,z) where the ,y,z make it an invalid expression (but "(x)" is a valid expression). a.b=(x) is NOT parsed as a method call but an assignment which is translated to a method call. It falls out naturally that a.b can also be part of a multiple assignment (on either side) - which may become multiple method calls.

Are you referring to ' "attribute = value" would otherwise be taken as a local variable assignment" '? What I'm talking about there is if you define a conventional attribute= method (no extra args) and you want to use it somewhere from the same object, you have to assign it like this:

 self.attribute = value

because this:

 attribute = value

would get interpreted as a local variable assignment (did 1.9 change this?). This RCR allows you to do the same thing to the LHS that you would normally do on the RHS to differentiate local variables and methods - add parentheses:

 attribute() = value


Oh, you're right, this never works:

  lhs = (a,b,c)

I'd forgotten.

  Are you referring to ' "attribute = value" would otherwise be
  taken as a local variable assignment" '?

No, I'm very familiar with that :-) The line that I didn't understand was:

  It is likely illegal to not get this method call confused with
  an assign (which also becomes a method call). 

I don't think assignments "become" method calls. Actually by the time the parser reaches the "=", it should know which it is (since the decision is based on the syntax of the LHS). But I haven't examined the parser.

As for this:

  attribute() = value

that's not good. The whole point of the "=" thing is to provide assignment syntax for attributes, so this kind of differentiation goes against that. I find self.attribute = x (which, after all, is just a receiver and a message) much more consistent with Ruby style and design than attribute() on the LHS.

(There's also the danger that people will start putting () all over the place in such cases. One already sees a fair number of ()'s cropping up for no good reason. It's probably better not to encourage it....)

David Black


My evidence of it being parsed as an assign is a) allowed spacing around the '=' (part of the method name) and b) looking at this stuff in the context of a multiple assign. Try this:

class X;def a;puts "a";"a";end;def a=(v);puts "a=#{v}";end;end class Y;def b;puts "b";"b";end;def b=(v);puts "b=#{v}";end;end x = X.new; y = Y.new x.a,y.b = y.b,x.a

b a a=b b=a => ["b", "a"]

If you want to be analogous to duck typing - it walks like, talks like, sounds like an assign, so it must be an assign. The actual assignment of each attribute in this multi-assign happens to be accomplished with a method call to the attribute name followed by an '='.

I don't use the extra () much on 0-arg method calls, but one could argue that it can help readability because you can easily distinguish a method call from a local variable. I would rather be forced to use extra () than an extra "self." when the semantics demand it to differentiate.


Here is a format corrected version of that multi-assign example:

 class X;def a;puts "a";"a";end;def a=(v);puts "a=#{v}";end;end
 class Y;def b;puts "b";"b";end;def b=(v);puts "b=#{v}";end;end
 x = X.new; y = Y.new
 x.a,y.b = y.b,x.a
 b
 a
 a=b
 b=a
 => ["b", "a"]


The self in

  self.x = y

is not "extra", though. Think of it the other way around: Ruby always requires an explicit receiver, except in the single case of a method whose receiver is 'self' and which is not a "setter" method.

As for (): "readability" is of course one of those things (like "Rubyishness" and "elegance") that one can't really discuss productively beyond a certain point. But I don't find

  x() = y

any more "readable" than

  self.x = y

since the latter conforms completely to the bread-and-butter method-calling syntax of Ruby and is unmistakeable. (Another one of those words :-)

As for assignments and parsing: I'm not clear on what level of "parsing" we're talking about, human or parser. I don't think the parser does duck typing :-) I can't get your assignment example to run. b and a are not initialized. What did you mean exactly?

In any case -- to bring this back to the RCR -- I actually think that the prospect of:

  x() = y

(i.e., the empty ()) all over the place is a bit of a deal-breaker for me. Beyond that I'm somewhat neutral.

David Black


Sorry, the first 4 lines in that previous multi-assign is the code. The rest is the output (and the result of the multi-assign).

The x() = y or x(*args) = y is an optional part of this RCR (it seems like a natural extension - but unnecessary). The meat of it is the same as RCR 157: obj.x(*args) = y.


I think dblack is right in that

  x() = y

feels less natural than

  self.x = y

I'm not sure if it should be explicitely forbidden, but if implementing obj.foo(bar) = qux does not already add support for the no-receiver case without much effort as well I think it should just keep resulting in a parse error.

I can live with it being possible, but would still like to see it discouraged (just like not indenting code properly).

-- Florian Gross


  I can live with it being possible, but would still like to see
  it discouraged (just like not indenting code properly). 

And we've seen how much attention people pay to that.... :-)


I think this is a very natural extension to attribute assignment. To me it seems illogical that you can write "file.pos = x" instead of "file.set_pos(x)", but have to write "file.set_pos(x,options)" instead of "file.pos(options) = x"... However, I believe that

  x() = y

is really a dirty hack. But the solution would be easy -- forbid parentheses when there is no argument.


I use this idiom all over the place,

 def returning value
   yield ; value
 end

and I'm a bit worried that this RCR would break usage such as this:

 returning result = "" do
   things.each { |x| result << foo(x) if bar(x) }
 end

Specifically, I'm unsure as to whether the above will be parsed like this,

 returning(result = "") do ... end

or like this:

 returning(result) = "" do ... end

I'm hoping that the answer is the former, and that the latter would in fact be a syntax error. One previous post suggested that the proper syntax for passing blocks to assignment operators would be as follows:

 returning result do ... end = ""

If the semantics are to be supported at all, I'm strongly in favor of the above syntax (as opposed to putting the block after the `= foo' part).

As for the general idea of letting assignment operators take arguments, I tend to agree. It's a natural, useful extension. Please ban pathological cases, though, as allowing them will only serve to prevent future syntax improvements in the name of backwards compatibility with weird code. (Yes, `foo() = bar', I'm looking at you.)

Anyway, voted in favor.

-- Daniel Brockman


This RCR should break no current code. The new functionality comes in for cases that are currently a syntax error. If syntactically it looks like you are assigning something to a method call (currently a syntax error), the functionality of this RCR comes into play.

In the example you gave, operator precedence makes it work the way you want, and that precedence should not change.

p.s. I would hate to read your code. I don't understand why you don't just use parentheses. It is much clearer with them.


Did anyone perhaps notice the similarity of the functionality this would enable to the LISP-esque setf?

  db.query( "SELECT name FROM widgets" ) = "paula"

seems strikingly similar, to me, to:

  (setf (query db "SELECT name FROM widgets") "paula")

In that sense, I'd like to think that it's a useful feature in terms of making the language more uniform. I was actually about to submit something similar before I read this RCR...


Strongly opposed 2
Opposed 2
Neutral 3
In favor 6
Strongly advocate 2
ruby picture
If you have registered at RCRchive, you may now sign in below. If you have not registered, you may sign up for a username and password. Registering enables you to submit new RCRs, and vote and leave comments on existing RCRs.
Your username:
Your password:

ruby picture

Powered by .