ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
From: "nobu (Nobuyoshi Nakada)" <noreply@ruby-lang.org>
To: ruby-core@ruby-lang.org
Subject: [ruby-core:109737] [Ruby master Feature#18951] Object#with to set and restore attributes around a block
Date: Sat, 27 Aug 2022 14:37:01 +0000 (UTC)	[thread overview]
Message-ID: <redmine.journal-98972.20220827143701.7941@ruby-lang.org> (raw)
In-Reply-To: redmine.issue-18951.20220801175226.7941@ruby-lang.org

Issue #18951 has been updated by nobu (Nobuyoshi Nakada).


I agree with the concept, but wonder about these names.
The meaning of `with_something_enabled` is obvious, but sole `with` feels vague a little, to me.

And `old_values` should be initialized as an empty hash, I think.

----------------------------------------
Feature #18951: Object#with to set and restore attributes around a block
https://bugs.ruby-lang.org/issues/18951#change-98972

* Author: byroot (Jean Boussier)
* Status: Open
* Priority: Normal
----------------------------------------
### Use case

A very common pattern in Ruby, especially in testing is to save the value of an attribute, set a new value, and then restore the old value in an `ensure` clause.

e.g. in unit tests

```ruby
def test_something_when_enabled
  enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
  # test things
ensure
  SomeLibrary.enabled = enabled_was
end
```

Or sometime in actual APIs:

```ruby
def with_something_enabled
  enabled_was = @enabled
  @enabled = true
  yield
ensure
  @enabled = enabled_was
end
```

There is no inherent problem with this pattern, but it can be easy to make a mistake, for instance the unit test example:

```ruby
def test_something_when_enabled
  some_call_that_may_raise
  enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true
  # test things
ensure
  SomeLibrary.enabled = enabled_was
end
```

In the above if `some_call_that_may_raise` actually raises, `SomeLibrary.enabled` is set back to `nil` rather than its original value. I've seen this mistake quite frequently.

### Proposal

I think it would be very useful to have a method on Object to implement this pattern in a correct and easy to use way. The naive Ruby implementation would be:

```ruby
class Object
  def with(**attributes)
    old_values = attributes.dup
    attributes.each_key do |key|
      old_values[key] = public_send(key)
    end
    begin
      attributes.each do |key, value|
        public_send("#{key}=", value)
      end
      yield
    ensure
      old_values.each do |key, old_value|
        public_send("#{key}=", old_value)
      end
    end
  end
end
```

NB: `public_send` is used because I don't think such method should be usable if the accessors are private.

With usage:

```ruby
def test_something_when_enabled
  SomeLibrary.with(enabled: true) do
    # test things
  end
end
```

```ruby
GC.with(measure_total_time: true, auto_compact: false) do
  # do something
end
```

### Alternate names and signatures

If `#with` isn't good, I can also think of:

  - `Object#set`
  - `Object#apply`

But the `with_` prefix is by far the most used one when implementing methods that follow this pattern.

Also if accepting a Hash is dimmed too much, alternative signatures could be:

  - `Object#set(attr_name, value)`
  - `Object#set(attr1, value1, [attr2, value2], ...)`





-- 
https://bugs.ruby-lang.org/

  reply	other threads:[~2022-08-27 14:37 UTC|newest]

Thread overview: 23+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-08-01 17:52 [ruby-core:109403] [Ruby master Feature#18951] Object#with to set and restore attributes around a block byroot (Jean Boussier)
2022-08-27 14:37 ` nobu (Nobuyoshi Nakada) [this message]
2022-08-27 14:58 ` [ruby-core:109738] " Eregon (Benoit Daloze)
2022-08-27 15:06 ` [ruby-core:109739] " byroot (Jean Boussier)
2022-08-27 15:14 ` [ruby-core:109740] " retro
2022-08-27 15:22 ` [ruby-core:109741] " byroot (Jean Boussier)
2022-08-29 12:08 ` [ruby-core:109766] " ufuk (Ufuk Kayserilioglu)
2022-08-29 12:31 ` [ruby-core:109767] " byroot (Jean Boussier)
2022-08-29 14:11 ` [ruby-core:109771] " ngan (Ngan Pham)
2022-08-31 20:05 ` [ruby-core:109801] " Dan0042 (Daniel DeLorme)
2022-08-31 21:12 ` [ruby-core:109803] " austin (Austin Ziegler)
2022-09-01 10:37 ` [ruby-core:109809] " p8 (Petrik de Heus)
2022-09-11  2:34 ` [ruby-core:109881] " retro
2022-09-11  2:49 ` [ruby-core:109882] " baweaver (Brandon Weaver)
2022-09-21  5:58 ` [ruby-core:109969] " ko1 (Koichi Sasada)
2022-09-21  6:03 ` [ruby-core:109970] " byroot (Jean Boussier)
2022-09-23 15:40 ` [ruby-core:110049] " Dan0042 (Daniel DeLorme)
2022-11-15  9:18 ` [ruby-core:110760] " byroot (Jean Boussier)
2022-12-01  7:36 ` [ruby-core:111111] " matz (Yukihiro Matsumoto)
2022-12-01  7:38 ` [ruby-core:111112] " byroot (Jean Boussier)
2022-12-10 17:10 ` [ruby-core:111252] " retro
2023-03-06  8:18 ` [ruby-core:112698] " byroot (Jean Boussier) via ruby-core
2023-03-17  1:15 ` [ruby-core:112920] " dsisnero (Dominic Sisneros) via ruby-core

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-list from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.ruby-lang.org/en/community/mailing-lists/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=redmine.journal-98972.20220827143701.7941@ruby-lang.org \
    --to=ruby-core@ruby-lang.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).