Feature #17942
openAdd a `initialize(public @a, private @b)` shortcut syntax for defining public/private accessors for instance vars as part of constructor
Description
This proposal builds on the proposed initialize(@a, @b)
instance var assignment shortcut syntax described in #15192.
- It allows you to add an optional
public
/protected
/private
modifier before any instance var parameter. Doing so automatically defines accessor methods (with the given access modifier; equivalent toattr_accessor
inside of apublic
/protected
/private
block) for the instance var it precedes. - If the visibility modifier is omitted, then it defaults to automatically no getter/setter methods for that instance var (it only does an assignment of that already-private instance var).
Parameter properties in TypeScript language¶
This is inspired by TypeScript's constructor(public a, private b)
syntax, which allows you to write this (REPL):
class Foo {
constructor(public a:number, public b:number, private c:number) {
}
}
instead of this:
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:
- https://www.typescriptlang.org/docs/handbook/2/classes.html#parameter-properties
- https://basarat.gitbook.io/typescript/future-javascript/classes#define-using-constructor
- https://kendaleiv.com/typescript-constructor-assignment-public-and-private-keywords/
Differences from TypeScript¶
I propose adding a similar feature to Ruby, but with following differences from TypeScript:
-
Use
@a
instead of barea
. This makes it much clearer that you are assigning directly to instance variables instead of to locals.- Rationale: The
@
is actually part of the instance variable name, and is inseparable from it. (This is also consistent with how the#
is part of the name itself in JavaScript's (Private instance fields).) - (
public a
would be a syntax error because there's no such thing as access modifiers for locals. Okay, I guess there's no such thing as access modifiers for instance vars either, which is why...)
- Rationale: The
-
Make the syntax for assigning to instance vars (
@a
) (the proposal in #15192) and defining accessor methods for those instance vars (public
/private
) separate/distinct.- In other words, rather than make the
public
/private
keywords a required part of the syntax like it is for TypeScript parameter properties, you could omit the modifier and it would still do the instance var _assignment*. - The
public
/private
access modifiers be an additional (optional) shortcut when you want to add an accessor method in addition to doing an assignment . - Unlike Java and TypeScript where you can add access modifiers to instance variables, in Ruby,
public
/private
can't be applied to instance variables (direct access is only possible from within the instance). So if we're going to allow apublic
/private
modifier here at all, They must refer to methods, specifically accessor methods for those instance variables.
- In other words, rather than make the
-
Keep it private by default (which of course
@a
by itself implies—it is private unless you add a public accessor).- (Rather than make it
public
by default like it is in TypeScript.) - Keeping instance variables completely private is probably what people will want most of the time, and we should optimize the ergonomics for the most common case.
- Private is a safer default, and should be assumed unless you explicitly ask for a public accessor to be added.
- I bet TypeScript made the
public
the default mostly to be consistent with JavaScript (which TypeScript compiles to): JavaScript (along with other languages like Java) allows direct access (no getter/setter neede) to instance properties/variables from objects outside the instance. JavaScript doesn't even have a way to make instance variables private (but hopefully will soon with this proposal to add#a
syntax for private properties).
- (Rather than make it
So this:
class Thing
def initialize(public @a, public @b, @c)
end
end
would be equivalent to this:
class Thing
attr_accessor :a, :b
def initialize(a, b, c)
@a = a
@b = b
@c = c
end
How is initialize(private @a)
different from initialize(@a)
?¶
Even though @a
by itself is already private...
- This defines a private accessor for that instance var, which lets you write
self.a =
instead of@a =
(if you want). - Having a concise way to do that is helpful, for example if you want to make it a matter of practice/policy to only set an instance variable by going through its setter method. (See discussion here.)
Why not just use initialize(private @a)
to be consistent with TypeScript spec?
- TypeScript's
public
/private
is not standard JavaScript. In fact, if the private methods/fields proposal had existed when TypeScript added parameter properties, I'd like to think that they might have actually made use of the new#b
syntax and gone with a terser syntax likeconstructor(public a, #b)
instead of ``constructor(public a, private b)`.
Upsides of this proposal¶
- Removes even more boilerplate (all those
attr_accessor
lines), much of the time
Downsides of this proposal¶
- Only provides a way to define both getter and setter at once. Doesn't provide a way to just define a getter and not a setter, for example.
- Doesn't seem like a big deal, however. You can just not use this feature and define the getter with
attr_reader :a
instead. Or define private getter/setter withprivate @a
and then override withattr_reader :a
to add a public getter (while keeping the private setter).
- Doesn't seem like a big deal, however. You can just not use this feature and define the getter with
Updated by TylerRick (Tyler Rick) over 3 years ago
- Subject changed from Add a `initialize(public @a, private @b)` shortcut syntax for defining public/private accessors for instance vars to Add a `initialize(public @a, private @b)` shortcut syntax for defining public/private accessors for instance vars as part of constructor
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
Updated by jeremyevans0 (Jeremy Evans) over 3 years ago
You should probably read @matz's response to a previous request for instance variable parameters: https://bugs.ruby-lang.org/issues/8563#note-3 (which he confirmed had not changed as of 2017: https://bugs.ruby-lang.org/issues/8563#note-18). Your proposal is more complex, but doesn't address the complaint @matz (Yukihiro Matsumoto) has regarding using instance variables as parameters. A new issue with your proposal is it would turn public
/private
into keywords, when they are currently just methods.
If you want really concise class definitions, use Struct
. It doesn't get much more concise than:
Thing = Struct.new(:a, :b, :c)
Updated by LevLukomskyi (Lev Lukomskyi) about 3 years ago
If you want really concise class definitions, use
Struct
. It doesn't get much more concise than:Thing = Struct.new(:a, :b, :c)
This construction is bad because:
- It looks awkward, and not consistent with the rest of the classes declarations in the project, also it leads to Rubocop offence because of constant case. You can do
class Thing < Struct.new(...)
which looks more consistent but it creates a redundant level of inheritance - It doesn't allow to mix positional and keyword args, eg.
Struct.new(:a, :b, :c, d: nil)
won't work - It doesn't allow to adjust initialization of one of the arguments, eg. I want
@b = b.to_i
and the rest initialized as usual.
The lack of this feature makes the language NOT FUN because you are forced to create a lot of duplication each time you create a basic class, eg.
class PollItem::ToggleVote
attr_reader :poll_item, :user, :voted, :ip_address
def initialize(poll_item, user, voted, ip_address:)
@poll_item = poll_item
@user = user
@voted = voted
@ip_address = ip_address
end
def perform; end
end
Here we see poll_item
, user
, voted
, ip_address
names are duplicated 4 times each – which means more work when you decide to add/remove/rename the argument, more code – more possibilities for an error. Duplication is NOT FUN – It's not a surprise why there are so many people advocating this feature.
This could theoretically be rewritten to something like this:
class PollItem::ToggleVote
attr_reader :poll_item, :user, :voted, :ip_address
def initialize(@poll_item, @user, @voted, @ip_address:)
end
def perform; end
end
Much cleaner! And then to something like this:
class PollItem::ToggleVote
def initialize(@poll_item, @user, @voted, @ip_address:)
init_readers
end
def perform; end
end
init_readers
would create attr_readers for all instance variables in initializer. public
/private
keywords as described in the issue are not ideal as they litter args and they will be repeated a lot, eg. def initialize(public @poll_item, public @user, public @voted, public @ip_address:)
, though it's better than nothing.
If I decide to customize one of argument, I could do this:
class PollItem::ToggleVote
def initialize(@poll_item, @user, @voted, ip_address:)
@ip_address = IpAddress.parse(ip_address)
init_readers
end
end
This would be fantastic! This would reduce duplication. This would be Concise – I love ruby especially because of this feat.
I saw Matz was against this feature, the main point was:
def initialize(@foo, @bar)
end
does not express intention of instance variable initialization
But – it does express – there is a word initialize
and then goes @foo
, it means "Please initialize @foo with whatever is passed here".
It'd be sad to block this very useful feature (I'm writing such classes every day for 10+ years) because of any tiny obstacles.
Updated by nobu (Nobuyoshi Nakada) about 2 years ago
This means you want only initialize
method to be parsed specially?
And when bypassing this method, e.g., Marshal.load
, no accessor will be defined?
Updated by sawa (Tsuyoshi Sawada) about 2 years ago
LevLukomskyi (Lev Lukomskyi) wrote in #note-6:
[Y]ou are forced to create a lot of duplication [...]
class PollItem::ToggleVote attr_reader :poll_item, :user, :voted, :ip_address def initialize(poll_item, user, voted, ip_address:) @poll_item = poll_item @user = user @voted = voted @ip_address = ip_address end
Here we see
poll_item
,user
,voted
,ip_address
names are duplicated 4 times
Not necessarily. You can do with 2 times (counting the use with attr_reader
):
def initialize(*args)
@poll_item, @user, @voted, @ip_address = args
end
I saw Matz was against this feature, the main point was:
def initialize(@foo, @bar) end
does not express intention of instance variable initialization
But – it does express – there is a word
initialize
and then goes@foo
, it means "Please initialize @foo with whatever is passed here".
No, it doesn't. It would mean "please initialize the newly created instance with whatever is passed here as the value of @foo
." In general, Ruby code foo.bar(baz)
translates to English as "do bar to foo using baz", not "foo does bar to baz."