Project

General

Profile

Actions

Feature #17942

open

Add a `initialize(public @a, private @b)` shortcut syntax for defining public/private accessors for instance vars as part of constructor

Added by TylerRick (Tyler Rick) over 3 years ago. Updated about 2 years ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:104213]

Description

This proposal builds on the proposed initialize(@a, @b) instance var assignment shortcut syntax described in #15192.

  1. 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 to attr_accessor inside of a public/protected/private block) for the instance var it precedes.
  2. 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:

Differences from TypeScript

I propose adding a similar feature to Ruby, but with following differences from TypeScript:

  1. Use @a instead of bare a. 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...)
  2. 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 a public/private modifier here at all, They must refer to methods, specifically accessor methods for those instance variables.
  3. 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).

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...

  1. This defines a private accessor for that instance var, which lets you write self.a = instead of @a = (if you want).
  2. 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 like constructor(public a, #b) instead of ``constructor(public a, private b)`.

Upsides of this proposal

  1. Removes even more boilerplate (all those attr_accessor lines), much of the time

Downsides of this proposal

  1. 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 with private @a and then override with attr_reader :a to add a public getter (while keeping the private setter).

Related issues 1 (1 open0 closed)

Related to Ruby master - Feature #5825: Sweet instance var assignment in the object initializerAssignedmatz (Yukihiro Matsumoto)Actions
Actions #1

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
Actions #2

Updated by TylerRick (Tyler Rick) over 3 years ago

  • Description updated (diff)
Actions #3

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

FYI: This is (partially) a duplicate of #5825, #8563, #12023, #12578, #12820, and #15192.

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:

  1. 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
  2. It doesn't allow to mix positional and keyword args, eg. Struct.new(:a, :b, :c, d: nil) won't work
  3. 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."

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0