[ruby-core:109803] [Ruby master Feature#18951] Object#with to set and restore attributes around a block
From:
"austin (Austin Ziegler)" <noreply@...>
Date:
2022-08-31 21:12:41 UTC
List:
ruby-core #109803
Issue #18951 has been updated by austin (Austin Ziegler).
Dan0042 (Daniel DeLorme) wrote in #note-10:
> Also really like the idea, also ambivalent about such a short and generic name.
>
> I think it would be good if the name emphasized a bit more that the original values will be restored after the block. Something in the vein of `temporarily_with`
- `#override_with`
- `#mut`
- `#mutate`
- `#borrow_with`
- `#with_restore`
- `#restore_do`
- `#tap_restore`
- `#tap_reset`
- `#override_reset`
- `#overlay_with`
- `#overlay`
- `#overlay_reset`
This could do something with `Object#extend` or maybe a temporary refinement (I have yet to *use* refinements, so I’m going to avoid trying to make an example that way).
```ruby
module RestorableOverlay
def overlay(values)
(@__restoreable_overlay__ ||= []) << {}
values.each_pair { |k, v|
@__restorable_overlay__[-1][k] = send(:"#{k}")
send(:"#{k}=", v)
}
end
def overlay_commit
@__restorable_overlay__.pop
end
def overlay_rollback
overlay_commit.each_pair { |k, v| send(:"#{k}=", v) }
# magic function that doesn’t exist
if @__restorable_overlay__.empty?
unextend RestorableOverlay
instance_variable_del(:@__restorable_overlay__)
end
end
end
class Object
def overlay_with(values)
extend(RestorableOverlay) unless extended_with?(RestorableOverlay)
if block_given?
begin
overlay(values)
yield
ensure
overlay_rollback
end
else
overlay(values)
end
end
end
```
I had done something *similar* back in 2004 while working with PDF::Writer (Ruby 1.8 mostly), originally released as https://github.com/halostatue/transaction-simple. An absolute memory hog, broke parent / child ownership cycles, and slow as can be, but it *worked* for the most part.
Maybe worth revisiting?
----------------------------------------
Feature #18951: Object#with to set and restore attributes around a block
https://bugs.ruby-lang.org/issues/18951#change-99044
* 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.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/
Unsubscribe: <mailto:ruby-core-request@ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>