Project

General

Profile

Actions

Feature #15192

open

Introduce a new "shortcut assigning" syntax to convenient setup instance variables

Added by jjyr (Jinyang Jiang) over 3 years ago. Updated 5 months ago.

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

Description

Motivation:

Introduce a new syntax for convenient setup instance variables for objects.

The problem:

Currently, setup instance variables in Ruby is too verbose.
Basically, we need to write the meaningless assigning code again and again to assign variables

class Person
  def initialize(name:, age:, gender:, country:)
    @name = name
    @age = age
    @gender = gender
    @country = country
  end
end


# we can use Struct to avoiding this

Person = Struct.new(:name, :age, :gender, :country, keyword_init: true)

# let's see a real-world case, which can't use Struct to describe an initializing process, from https://github.com/ciri-ethereum/ciri/blob/748985ccf7a620a2e480706a5a6b38f56409d487/lib/ciri/devp2p/server.rb#L54
# Because we want to do something more than just assigning instance variables

class Server
      def initialize(private_key:, protocol_manage:, bootstrap_nodes: [],
                     node_name: 'Ciri', tcp_host: '127.0.0.1', tcp_port: 33033)
        @private_key = private_key
        @node_name = node_name
        @bootstrap_nodes = bootstrap_nodes
        @protocol_manage = protocol_manage
        server_node_id = NodeID.new(@private_key)
        caps = [Cap.new(name: 'eth', version: 63)]
        @handshake = ProtocolHandshake.new(version: BASE_PROTOCOL_VERSION, name: @node_name, id: server_node_id.id, caps: caps)
        @tcp_host = tcp_host
        @tcp_port = tcp_port
        @dial = Dial.new(bootstrap_nodes: bootstrap_nodes, private_key: private_key, handshake: @handshake)
        @network_state = NetworkState.new(protocol_manage)
        @dial_scheduler = DialScheduler.new(@network_state, @dial)
      end
end


# Introduce a new "shortcut assigning" syntax for convenient setup

class Person
  # use @ prefix to describe instance variables.
  def initialize(@name:, @age:, @gender:, @country:)
  end

  # equal to
  def initialize2(name:, age:, gender:, country:)
    @name = name
    @age = age
    @gender = gender
    @country = country
  end

  # it should also work on position style arguments
  def initialize2(@name, @age, @gender, @country)
  end
end

# Our real-world case can be rewritten as below
class Server
      def initialize(@private_key:, @protocol_manage:, @bootstrap_nodes: [],
                     @node_name: 'Ciri', @tcp_host: '127.0.0.1', @tcp_port: 33033)
        server_node_id = NodeID.new(@private_key)
        caps = [Cap.new(name: 'eth', version: 63)]
        @handshake = ProtocolHandshake.new(version: BASE_PROTOCOL_VERSION, name: @node_name, id: server_node_id.id, caps: caps)
        @dial = Dial.new(bootstrap_nodes: @bootstrap_nodes, private_key: @private_key, handshake: @handshake)
        @network_state = NetworkState.new(@protocol_manage)
        @dial_scheduler = DialScheduler.new(@network_state, @dial)
      end
end

# consider to keep consistency, this "shortcut assigning" syntax should work for non-initialize methods
class Foo
  def bar(@still_works)
    p @still_works
  end
end

Related issues 2 (1 open1 closed)

Related to Ruby master - Feature #12820: Shorter syntax for assigning a method argument to an instance variableRejectedActions
Is duplicate of Ruby master - Feature #5825: Sweet instance var assignment in the object initializerAssignedmatz (Yukihiro Matsumoto)Actions
Actions #1

Updated by matz (Yukihiro Matsumoto) over 3 years ago

  • Related to Feature #12820: Shorter syntax for assigning a method argument to an instance variable added
Actions #2

Updated by jjyr (Jinyang Jiang) over 3 years ago

  • Description updated (diff)

Updated by shyouhei (Shyouhei Urabe) over 3 years ago

Matz thinks this is handy only when you write #initialize, which only is not worth adding a new syntax for methods in general. Do you have any situations other than #initialize where this is useful?

Updated by shyouhei (Shyouhei Urabe) over 3 years ago

This isn't Matz's but my experience. When "we want to do something more than just assigning instance variables", that "something" tends to include cancellation of creating new object -- maybe because we are returning a cached instance, or because we are raising an exception. In order to properly handle such situations #initialize tends to be too late to "do something". When I write a complex constructor that tends to happen inside of .new directly, and #initialize eventually becomes a series of instance variable assignments. One of such example is: https://github.com/shyouhei/xmp2assert/blob/master/lib/xmp2assert/quasifile.rb

Updated by shevegen (Robert A. Heiler) over 3 years ago

I personally tend to use (slower) setter-methods rather than initialization within initialize()
itself. Normally only for somewhat larger classes though; for small classes that do not do much,
I often don't bother writing setter methods; and they often don't need setter methods.

So:

def initialize(*i)
  reset # <- a method to setup the default state for the objects
  set_commandline_arguments(i) # <- and often keeping track of the commandline arguments passed in via ARGV
  # continue with the assignments via method calls, often in a method called run() that I like to use and call here
end

Then again I don't think that my ruby code/style is very commonly used either. :)

We also saw the comparison to Struct in ruby. While I think structs are great, in actual
practice I also rarely use structs. Oddly enough, I tend to really just write out the
definitions of methods on my own, much more often than using any of the attr* "shortcuts"
too.

I understand the shortcut idea behind the proposal, e. g. to get rid of some lines
of code that is used for assignment to instance variable.

Personally I have no strong opinion either way since I can understand both arguments but
I think that aside from what shyouhei wrote, matz is also not too fond of the syntax.

In the discussion in the other thread in particular he did not like the:

def initialize(@foo, @bar)

notation. So I think the additional problem here is clarity of intent and consistency
through idiomatic ruby. People will also of course make use of it a lot in their own
code (once something is made possible, people will use it) and in this case I am not
entirely sure whether that change in particular would be very good, just syntax-wise
alone. (I also tend to watch crystal, which uses a syntax similar to this,
but crystal also diverged in some strange ways, in my opinion, syntax-wise - e. g
"abstract" classes or macros, which I find very strange.)

  • You (or others) could try to re-evaluate the proposals at some later time in the
    future since sometimes other parts of ruby or the usage may change (for example, see
    the change that allows unicode used as a constant/name of classes, which was not
    possible before not that long ago).

Updated by marcandre (Marc-Andre Lafortune) over 3 years ago

  • Assignee set to matz (Yukihiro Matsumoto)

This has been requested a lot :-)

I still feel like it would be very practical, introduces no incompatibility, and is also very intuitive (at least to me). I feel that no Rubyist would have trouble learning what def initialize(@something, @some_option: nil) would mean; it would take about 5 seconds.

Actions #7

Updated by mame (Yusuke Endoh) over 3 years ago

  • Related to Feature #5825: Sweet instance var assignment in the object initializer added

Updated by mame (Yusuke Endoh) over 3 years ago

At the previous deverlopers' meeting (Sep.), I brought #5825 up for discussion. Matz said that he still dislikes this syntax. (Personally I like it.)

Updated by jjyr (Jinyang Jiang) over 3 years ago

I am surprised this syntax has been repeatedly requested and rejected since 7 years ago.

Write assigning code maybe is not a big problem to developers, but cause Ruby has Struct to solve the assigning problem, so at least it is a problem worth to solve.

But Struct is so limited, even default values require rewrite initialize method to implement.

IMO this syntax is useful and simple enough.

Updated by matz (Yukihiro Matsumoto) over 3 years ago

I still don't agree with the proposed syntax.
The option I can accept is something like (as is not the only option):

def initialize(name: as @name, age: as @age)
  ...
end

Matz.

Updated by shevegen (Robert A. Heiler) over 3 years ago

marcandre wrote

I feel that no Rubyist would have trouble learning what def
initialize(@something, @some_option: nil) would mean; it would
take about 5 seconds.

I think this is a bit difficult to say, because we can always reason that one small feature that
is added, is just one more tiny little baby step.

But say that you combine lots of baby steps ... all very simple on their own, but together they
add to the complexity or spaghetti design of a language. Like perhaps PHP.

In Ruby we can also omit () in method definitions like:

def initialize @a, @b: nil, @c: { cat: :tom }

I am not sure if this is an improvement. To me it does not seem very pretty. Of course I am biased since
I also prefer () in method definitions if they have arguments; although I think it is fine that ruby does
not mind omitting the (). For my brain, I like the () for visual separation. I am not sure I like the @foo
syntax that much on the left hand side. What about syntax like @a = @b? I mean, I assume we assign the
value of @b towards @a ... but ideally I'd prefer to not want to see syntax like that in method definitions;
or having to look closely for : { and @. May be a matter of personal preference too.

Once added, it would also be harder to remove the syntax again, in the sense of people who may like
syntax (like @@ class variables) so I am not entirely sure if it's a great idea. But I don't want to
be too discouraging since it is mostly just a difference of opinions.

Before I write too much, I'll finish by saying that I personally am not that fully convinced that it is
such a good idea to have the proposed feature associated with that particular syntax, even though it is
repeated every now and then (but so were ideas such as removing Symbols; I think jeremy evans wrote good
comments about that other situation). I am also not that convinced that a different syntax will be of
more help, either. Perhaps I am becoming more conservative as I become older. (One problem I see with
a longer syntax is that some of the advantage is lost by short-cutting assignment.)

On a side note, since crystal has that syntax and functionality, people could actually try out crystal and
see how that goes in regards to the "shortcut assignment" after a few months. :) (I mean really mostly
unbiased people here, though ideally those who already know ruby, even though that leads to some bias; and
can then compare to crystal, syntax-wise. Personally I feel that the ruby syntax is better than crystal's
syntax, due to various reason, but I don't want to digress here towards another programming language too
much.)

Updated by jsc (Justin Collins) over 3 years ago

jjyr (Jinyang Jiang) wrote:

I am surprised this syntax has been repeatedly requested and rejected since 7 years ago.

Write assigning code maybe is not a big problem to developers, but cause Ruby has Struct to solve the assigning problem, so at least it is a problem worth to solve.

But Struct is so limited, even default values require rewrite initialize method to implement.

IMO this syntax is useful and simple enough.

As someone who has been writing Ruby for over 10 years, this syntax is exactly that I would like.

I grow really tired of writing

def initialize(a, b, c)
  @a = a
  @b = b
  @c = c
end

This would be perfect:

def initialize(@a, @b, @c)
end

I'm a little bit sad Matz is against this syntax, as it seems so natural to me.

Updated by Dan0042 (Daniel DeLorme) almost 3 years ago

Instead of an entirely new syntax maybe something like this would be good enough?

def set_ivars_from_locals(binding, except: [])
  names = binding.local_variables - except
  values = eval("["+names.join(",")+"]", binding)
  names.zip(values) do |name,value|
    instance_variable_set("@#{name}", value)
  end
end

class A
  def initialize(a,b,c,d,e,f,g)
    set_ivars_from_locals(binding, except: %i[d g])
  end
end

A.new(1,2,3,4,5,6,7) #=> #<A:0x000055cb696bb8b0 @a=1, @b=2, @c=3, @e=5, @f=6>
Actions #14

Updated by nobu (Nobuyoshi Nakada) almost 3 years ago

  • Related to deleted (Feature #5825: Sweet instance var assignment in the object initializer)
Actions #15

Updated by nobu (Nobuyoshi Nakada) almost 3 years ago

  • Is duplicate of Feature #5825: Sweet instance var assignment in the object initializer added
Actions #16

Updated by nobu (Nobuyoshi Nakada) almost 3 years ago

  • Has duplicate Feature #16095: 2 Features: remove (simplify) 'new' keyword and Property Shorthand added
Actions #17

Updated by nobu (Nobuyoshi Nakada) almost 3 years ago

  • Has duplicate deleted (Feature #16095: 2 Features: remove (simplify) 'new' keyword and Property Shorthand)

Updated by TylerRick (Tyler Rick) 12 months ago

Same feature in TypeScript

It's worth mentioning that other languages have a shortcut for assignment var assignment directly from constructor parameters. So it seems especially painful that Ruby, despite being so beautifully elegant and succinct in other areas, still has no such shortcut for this.

One of those other languages (CoffeeScript) is dead now, but TypeScript remains very much alive and allows you to write this (REPL):

class Foo {
    constructor(public a:number, public b:number, private c:number) {
    }
}

instead of this boilerplate:

class Foo {
    constructor(a, b, c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }
}

(The public/private access modifiers actually disappear in the transpiled JavaScript code because it's only the TypeScript compiler that enforces those access modifiers, and it does so at compile time rather than at run time.)

Further reading:

I actually wouldn't mind being able to use public/private modifiers on instance var parameters in Ruby, too, but if we did, I would suggest making that be an additional optional shortcut (for defining accessor methods for those instance vars) that builds on top of the instance var assignment parameter syntax described here. (See more detailed proposal in #17942.) Accessors are more of a secondary concern to me: we can already define accessors pretty succinctly with attr_accessor and friends. The bigger pain point that I'm much more interested in having a succinct shortcut for is instance var assignment in constructors.

initialize(@a, @b, @c) syntax

jsc (Justin Collins) wrote in #note-12:

jjyr (Jinyang Jiang) wrote:

I am surprised this syntax has been repeatedly requested and rejected since 7 years ago.

...

As someone who has been writing Ruby for over 10 years, this syntax is exactly that I would like.

I grow really tired of writing

def initialize(a, b, c)
  @a = a
  @b = b
  @c = c
end

This would be perfect:

def initialize(@a, @b, @c)
end

I'm a little bit sad Matz is against this syntax, as it seems so natural to me.

Me too!! I've been writing Ruby for over 15 years, and this syntax seems like the most obvious, simple, natural, clear, unsurprising, and Ruby-like.

I believe it would be readily understood by any Rubyist without any explanation required.

Even if you saw it for the first time, I can't think of any way you could miss or misinterpret its meaning:

  • since @a is in the same position as a local variable a would normally be, it seems abundantly clear that instead of assigning to a local variable, we're just assigning to the variable @a instead
  • and of course you can reference the @a variable in the constructor body, too, exactly the same as you could with a local variable a passed as an argument.

Please??

Almost every time I write a new class in Ruby, I wish for this feature and wonder if we'll ever get it. Can we please?

A workaround pattern

In the meantime, I've taken to defining my constructor and list of public accessors (if any) like this:

    attr_reader \
                   :a, :b
    def initialize( a,  b)
                   @a, @b =
                    a,  b
    end

... which is still horrendously boilerplatey and ugly, and probably most of you will hate it — but by lining up the duplicated symbols into a table of columns, I like that I can at least more easily see the ugly duplication and cross-check that I've spelled them all correctly and handled them all consistently. :shrug:

Updated by Dan0042 (Daniel DeLorme) 5 months ago

The syntax def name(external_name: default_value => internal_name) has been suggested twice independently (#16460, #18402) so I think that would be fairly intuitive to any rubyist. And if instance variables were allowed as the internal_name, I think this would be a great way to solve this very-often-requested feature.

Like this:

      def initialize(private_key:          => @private_key,
                     protocol_manage:      => @protocol_manage,
                     bootstrap_nodes: []   => @bootstrap_nodes,
                     node_name: 'Ciri'     => @node_name,
                     tcp_host: '127.0.0.1' => @tcp_host,
                     tcp_port: 33033       => @tcp_port)
        server_node_id = NodeID.new(@private_key)
        caps = [Cap.new(name: 'eth', version: 63)]
        @handshake = ProtocolHandshake.new(version: BASE_PROTOCOL_VERSION, name: @node_name, id: server_node_id.id, caps: caps)
        @dial = Dial.new(bootstrap_nodes: bootstrap_nodes, private_key: private_key, handshake: @handshake)
        @network_state = NetworkState.new(protocol_manage)
        @dial_scheduler = DialScheduler.new(@network_state, @dial)
      end

And it's very similar to Matz' own preference:

matz (Yukihiro Matsumoto) wrote in #note-10:

I still don't agree with the proposed syntax.
The option I can accept is something like (as is not the only option):

def initialize(name: as @name, age: as @age)
  ...
end

Updated by austin (Austin Ziegler) 5 months ago

Dan0042 (Daniel DeLorme) wrote in #note-19:

The syntax def name(external_name: default_value => internal_name) has been suggested twice independently (#16460, #18402) so I think that would be fairly intuitive to any rubyist. And if instance variables were allowed as the internal_name, I think this would be a great way to solve this very-often-requested feature.

Like this:

      def initialize(private_key:          => @private_key,
                     protocol_manage:      => @protocol_manage,
                     bootstrap_nodes: []   => @bootstrap_nodes,
                     node_name: 'Ciri'     => @node_name,
                     tcp_host: '127.0.0.1' => @tcp_host,
                     tcp_port: 33033       => @tcp_port)
        server_node_id = NodeID.new(@private_key)
        caps = [Cap.new(name: 'eth', version: 63)]
        @handshake = ProtocolHandshake.new(version: BASE_PROTOCOL_VERSION, name: @node_name, id: server_node_id.id, caps: caps)
        @dial = Dial.new(bootstrap_nodes: bootstrap_nodes, private_key: private_key, handshake: @handshake)
        @network_state = NetworkState.new(protocol_manage)
        @dial_scheduler = DialScheduler.new(@network_state, @dial)
      end

I would personally prefer as over => for this, as it doesn’t increase the number of ways that => gets used in Ruby. Like it or not, Ruby has a reputation for being almost as full of line-noise as Perl, and this pushes things in the wrong direction.

 def initialize(private_key: as @private_key, for: 'Ciri' as node_name)
end

Updated by Dan0042 (Daniel DeLorme) 5 months ago

austin (Austin Ziegler) wrote in #note-20:

I would personally prefer as over => for this, as it doesn’t increase the number of ways that => gets used in Ruby.

I also like as, it's beautifully readable. But it would be a new keyword, and new keywords are almost never introduced in ruby. Even pattern matching was built out of existing keywords. And I can't agree that this is a "new way" of using =>; it has pretty much the same semantics as rightward assignment.

Updated by austin (Austin Ziegler) 5 months ago

Dan0042 (Daniel DeLorme) wrote in #note-21:

austin (Austin Ziegler) wrote in #note-20:

I would personally prefer as over => for this, as it doesn’t increase the number of ways that => gets used in Ruby.

I also like as, it's beautifully readable. But it would be a new keyword, and new keywords are almost never introduced in ruby. Even pattern matching was built out of existing keywords. And I can't agree that this is a "new way" of using =>; it has pretty much the same semantics as rightward assignment.

I would rather call it keyword-ish, as it would only be used in cases that are not currently legal syntax in any case. However, I think that this is a new way of using => here, which is essentially foo:=>bar for a required keyword argument foo that gets renamed to bar. As far as I understand rightward assignment (I’m not currently using Ruby 3), that’s not something that is legal. (This is, in many ways, an approximation of Elixir’s multi-head pattern matching, which is IMO much more elegant than what can be done with Ruby here. That’s not Ruby’s fault; this is a new-ish feature request on top of a language with thirty years history.)

Updated by nobu (Nobuyoshi Nakada) 5 months ago

austin (Austin Ziegler) wrote in #note-22:

I would rather call it keyword-ish, as it would only be used in cases that are not currently legal syntax in any case.

Then what about def name(external_name: default_value alias internal_name)?
This makes the purpose very clear.

I prefer => in these, though.

Updated by austin (Austin Ziegler) 5 months ago

nobu (Nobuyoshi Nakada) wrote in #note-23:

austin (Austin Ziegler) wrote in #note-22:

I would rather call it keyword-ish, as it would only be used in cases that are not currently legal syntax in any case.

Then what about def name(external_name: default_value alias internal_name)?
This makes the purpose very clear.

I prefer => in these, though.

alias would not be a bad choice. I think that the use of => starts getting opaque, and I say this as someone who’s used Ruby for nearly 20 years now.

def name(for: { "a" => 0, "b" => 1 } => for_target, bar: => bar_target)

There’s nothing about that to me that says that "parameter for is being given the name for_target with a default value", and bar: => bar_target seems like it's introducing an entirely new sigil complex (because people will try to write it as bar:=>bar_target.

Rightward assignment has been added, though, so maybe people who have started using it will get more out of it than I do. In part because I have gems that need to support older versions of Ruby, I don’t see myself starting to use rightward assignment for years to come.

Updated by nobu (Nobuyoshi Nakada) 5 months ago

nobu (Nobuyoshi Nakada) wrote in #note-23:

Then what about def name(external_name: default_value alias internal_name)?
This makes the purpose very clear.

Noticed that the proposal is not aliasing, but assignment.
That means it should be the following and alias is not a right word?

def name(f: => x)
  p [f, x] #=> [:a, :a]
  x = :b
  p [f, x] #=> [:a, :b]
end
name(f: :a)

Updated by Dan0042 (Daniel DeLorme) 5 months ago

austin (Austin Ziegler) wrote in #note-22:

As far as I understand rightward assignment (I’m not currently using Ruby 3), that’s not something that is legal.

Of course it's not currently legal to use rightward assignment in the method signature, but I think if you compare with positional arguments it's easy to see the similarity:
def foo(name = 'Ciri' => node_name)
Would be pretty much equivalent to the currently valid:
name = 'Ciri' => node_name
And I think it's not much of a stretch to go from name='Ciri' => node_name to name:'Ciri' => node_name, at least for the method signature.

Now if only name = 'Ciri' => @node_name was legal (#18408) it would also make sense to use it in the method signature.

austin (Austin Ziegler) wrote in #note-24:

def name(for: { "a" => 0, "b" => 1 } => for_target, bar: => bar_target)

You have to admit that example is a bit contrived. I've never seen such a hash used as a default value. :-)
But really I think everyone understands that the parameters in the method signature have slightly different semantics than the same syntax elsewhere. Depending on the presence of def, foo(bar=42) is a default value, not an assignment. foo(bar:) is a required keyword argument, not a "hash value omission". Etc.

nobu (Nobuyoshi Nakada) wrote in #note-25:

Noticed that the proposal is not aliasing, but assignment.
That means it should be the following and alias is not a right word?

While aliasing is not impossible, I would prefer to keep it simple; assignment seems much easier to understand.

Updated by austin (Austin Ziegler) 5 months ago

Dan0042 (Daniel DeLorme) wrote in #note-26:

austin (Austin Ziegler) wrote in #note-22:

As far as I understand rightward assignment (I’m not currently using Ruby 3), that’s not something that is legal.

Of course it's not currently legal to use rightward assignment in the method signature, but I think if you compare with positional arguments it's easy to see the similarity:
def foo(name = 'Ciri' => node_name)
Would be pretty much equivalent to the currently valid:
name = 'Ciri' => node_name
And I think it's not much of a stretch to go from name='Ciri' => node_name to name:'Ciri' => node_name, at least for the method signature.

Now if only name = 'Ciri' => @node_name was legal (#18408) it would also make sense to use it in the method signature.

The ship has sailed on rightward assignment (I think it’s a mistake and unforgivably ugly, I don’t understand the use case at all, and I suspect that I will never use it), but my comment was specifically about => node_name, which is the case for name: => node_name.

austin (Austin Ziegler) wrote in #note-24:

def name(for: { "a" => 0, "b" => 1 } => for_target, bar: => bar_target)

You have to admit that example is a bit contrived. I've never seen such a hash used as a default value. :-)
But really I think everyone understands that the parameters in the method signature have slightly different semantics that the same syntax elsewhere. Depending on the presence of def, foo(bar=42) is a default value, not an assignment. foo(bar:) is a required keyword argument, not a "hash value omission". Etc.

I have used a populated hash as a default value. I haven’t done so frequently, but there are cases where it makes sense.

Actions

Also available in: Atom PDF