Skip to content

Commit bf77067

Browse files
committed
Advent of Changelog: Day 13
1 parent 7b833cc commit bf77067

File tree

1 file changed

+156
-68
lines changed

1 file changed

+156
-68
lines changed

_src/3.3.md

Lines changed: 156 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ description: Ruby 3.3 full and annotated changelog
2727
* **Code:**
2828
```ruby
2929
[1, 2, 3].pack('r*')
30-
# Ruby 3.1: "", no warning
31-
# Ruby 3.2: "", warning: unknown pack directive 'r' in 'r*'
32-
# Ruby 3.3: in `pack': unknown pack directive 'r' in 'r*' (ArgumentError)
30+
# Ruby 3.1:
31+
# => "", no warning
32+
# Ruby 3.2:
33+
# => "", warning: unknown pack directive 'r' in 'r*'
34+
# Ruby 3.3:
35+
# in `pack': unknown pack directive 'r' in 'r*' (ArgumentError)
3336
```
3437
* **Notes:**
3538

@@ -66,9 +69,11 @@ Two methods to accept an integer file descriptor as an argument: `for_fd` create
6669
#=> "/home/zverok/projects/ruby/doc" -- the current path have changed successfully
6770

6871
# A block form of fchdir is available, like for a regular .chdir:
69-
Dir.fchdir(Dir.new('NEWS').fileno) do
72+
Dir.fchdir(Dir.new('NEWS').fileno) do |*args|
73+
p args #=> [] -- no arguments are passed into the block
7074
p Dir.pwd #=> "/home/zverok/projects/ruby/doc/NEWS"
71-
end
75+
'return value'
76+
end #=> "return value"
7277
Dir.pwd #=> "/home/zverok/projects/ruby/doc" -- back to the path before the block
7378
```
7479
* **Notes:**
@@ -89,9 +94,11 @@ An instance method version of [Dir.chdir](https://docs.ruby-lang.org/en/master/D
8994
Dir.pwd #=> "/home/zverok/projects/ruby/doc"
9095

9196
# The block form works, too:
92-
Dir.new('NEWS').chdir do
97+
Dir.new('NEWS').chdir do |*args|
98+
p args #=> [] -- no arguments are passed into the block
9399
Dir.pwd #=> "/home/zverok/projects/ruby/doc/NEWS"
94-
end
100+
'return value'
101+
end #=> "return value"
95102
Dir.pwd #=> "/home/zverok/projects/ruby/doc"
96103
```
97104

@@ -118,63 +125,63 @@ Allows to assign a string to be rendered as class/module's `#name`, without assi
118125
* **Documentation:** [Module#set_temporary_name](https://docs.ruby-lang.org/en/master/Module.html#method-i-set_temporary_name)
119126
* **Code:**
120127
```ruby
121-
dynamic_class = Class.new do
122-
def foo; end
123-
end
128+
dynamic_class = Class.new do
129+
def foo; end
130+
end
124131

125-
dynamic_class.name #=> nil
132+
dynamic_class.name #=> nil
126133

127-
# For dynamic classes, representation of related values is frequently unreadable:
128-
dynamic_class #=> #<Class:0x0...>
129-
instance = dynamic_class.new #=> #<#<Class:0x0...>:0x0...>
130-
instance.method(:foo) #=> #<Method: #<Class:0x0...>#foo() ...>
134+
# For dynamic classes, representation of related values is frequently unreadable:
135+
dynamic_class #=> #<Class:0x0...>
136+
instance = dynamic_class.new #=> #<#<Class:0x0...>:0x0...>
137+
instance.method(:foo) #=> #<Method: #<Class:0x0...>#foo() ...>
131138

132-
dynamic_class::Nested = Module.new
133-
dynamic_class::Nested #=> #<Class:0x0...>::Nested
139+
dynamic_class::Nested = Module.new
140+
dynamic_class::Nested #=> #<Class:0x0...>::Nested
134141

135-
# After assigning the temporary name, representation becomes more convenient:
136-
dynamic_class.set_temporary_name("MyDSLClass(with description)")
142+
# After assigning the temporary name, representation becomes more convenient:
143+
dynamic_class.set_temporary_name("MyDSLClass(with description)")
137144

138-
dynamic_class #=> MyDSLClass(with description)
139-
instance #=> #<MyDSLClass(with description):0x0...>
140-
instance.method(:foo) #=> #<Method: MyDSLClass(with description)#foo() ...>
145+
dynamic_class #=> MyDSLClass(with description)
146+
instance #=> #<MyDSLClass(with description):0x0...>
147+
instance.method(:foo) #=> #<Method: MyDSLClass(with description)#foo() ...>
141148

142-
# Note that module constant names are assigned at the moment of their creation,
143-
# and don't change when the temporary name is assigned:
144-
dynamic_class::OtherNested = Module.new
149+
# Note that module constant names are assigned at the moment of their creation,
150+
# and don't change when the temporary name is assigned:
151+
dynamic_class::OtherNested = Module.new
145152

146-
dynamic_class::Nested #=> #<Class:0x0...>::Nested
147-
dynamic_class::OtherNested #=> MyDSLClass(with description)::OtherNested
153+
dynamic_class::Nested #=> #<Class:0x0...>::Nested
154+
dynamic_class::OtherNested #=> MyDSLClass(with description)::OtherNested
148155

149-
# Assigning names that correspond to constant name rules is prohibited:
150-
dynamic_class.set_temporary_name("MyClass")
151-
# `set_temporary_name': the temporary name must not be a constant path to avoid confusion (ArgumentError)
152-
dynamic_class.set_temporary_name("MyClass::NestedName")
153-
# `set_temporary_name': the temporary name must not be a constant path to avoid confusion (ArgumentError)
156+
# Assigning names that correspond to constant name rules is prohibited:
157+
dynamic_class.set_temporary_name("MyClass")
158+
# `set_temporary_name': the temporary name must not be a constant path to avoid confusion (ArgumentError)
159+
dynamic_class.set_temporary_name("MyClass::NestedName")
160+
# `set_temporary_name': the temporary name must not be a constant path to avoid confusion (ArgumentError)
154161

155-
# When the module with a temporary name is put into a constant,
156-
# it receives a permanent name, which can't be changed anymore
157-
C = dynamic_class
162+
# When the module with a temporary name is put into a constant,
163+
# it receives a permanent name, which can't be changed anymore
164+
C = dynamic_class
158165

159-
# It affects all associated values (including modules)
166+
# It affects all associated values (including modules)
160167

161-
dynamic_class #=> C
162-
instance #=> #<C:0x0...>
163-
instance.method(:foo) #=> #<Method: C#foo() ...>
164-
dynamic_class::Nested #=> C::Nested
165-
dynamic_class::OtherNested #=> C::OtherNested
168+
dynamic_class #=> C
169+
instance #=> #<C:0x0...>
170+
instance.method(:foo) #=> #<Method: C#foo() ...>
171+
dynamic_class::Nested #=> C::Nested
172+
dynamic_class::OtherNested #=> C::OtherNested
166173

167-
dynamic_class.set_temporary_name("Can I have it back?")
168-
# `set_temporary_name': can't change permanent name (RuntimeError)
174+
dynamic_class.set_temporary_name("Can I have it back?")
175+
# `set_temporary_name': can't change permanent name (RuntimeError)
169176

170-
# `nil` can be used to cleanup a temporary name:
171-
other_class = Class.new
172-
other_class.set_temporary_name("another one")
173-
other_class #=> another one
174-
other_class.set_temporary_name(nil)
175-
other_class #=> #<Class:0x0...>
177+
# `nil` can be used to cleanup a temporary name:
178+
other_class = Class.new
179+
other_class.set_temporary_name("another one")
180+
other_class #=> another one
181+
other_class.set_temporary_name(nil)
182+
other_class #=> #<Class:0x0...>
176183
```
177-
* **Notes:** Any phrase that used as a temporary name would be used verbatim; this might create very confusing `#inspect` results and error messages; so it is advised to use strings somehow implying that the name belong to a module. Imagine we wrap into classes with temporary names RSpec-style examples, and then there is a typo in such example:
184+
* **Notes:** Any phrase that used as a temporary name would be used verbatim; this might create very confusing `#inspect` results and error messages; so it is advised to use strings somehow implying that the name belong to a module. Imagine we wrap into classes with temporary names RSpec-style examples, and then there is a typo in the body of such example:
178185
```ruby
179186
it "works as a calculator" do
180187
expec(2+2).to eq 4
@@ -200,6 +207,20 @@ A new "weak map" concept implementation. Unlike `ObjectSpace::WeakMap`, it compa
200207
* **Discussion:** [Feature #18498]
201208
* **Documentation:** [ObjectSpace::WeakKeyMap](https://docs.ruby-lang.org/en/master/ObjectSpace/WeakKeyMap.html)
202209
* **Code:**
210+
```ruby
211+
map = ObjectSpace::WeekMap.new
212+
213+
key = "foo"
214+
map[key] = true
215+
map["foo"] #=> true
216+
217+
key = nil
218+
GC.start
219+
220+
map["foo"] #=> nil
221+
222+
TODO
223+
```
203224
* **Notes:** The class interface is significantly leaner than `WeakMap`'s, and doesn't provide any kind of iteration methods (which is very hard to implement and use correctly with weakly-referenced objects), so the new class is more like a black box with associations than a collection.
204225

205226
### `ObjectSpace::WeakMap#delete`
@@ -232,13 +253,14 @@ A new "weak map" concept implementation. Unlike `ObjectSpace::WeakMap`, it compa
232253

233254
### `Proc#dup` and `#clone` call `#initialize_dup` and `#initialize_copy`
234255

235-
* **Reason:** A fix for an old inconsistency: `Object`'s `#dup` and `#clone` methods docs
256+
* **Reason:** A fix for an old inconsistency: `Object`'s `#dup` and `#clone` methods docs claimed that corresponding copying constructors would be called on object cloning/duplication, yet it was not true for `Proc`.
236257
* **Discussion:** [Feature #19362]
237258
* **Documentation:** — (Adheres to the behavior described for [Object#dup](https://docs.ruby-lang.org/en/master/Object.html#method-i-dup) and [#clone](https://docs.ruby-lang.org/en/master/Kernel.html#method-i-clone))
238259
* **Code:**
239260
```ruby
240-
# The examples would work the same with
261+
# The examples would work the same way with
241262
# #dup/#initialize_dup and #clone/#initialize_copy
263+
242264
class TaggedProc < Proc
243265
attr_reader :tag
244266

@@ -279,19 +301,21 @@ A new "weak map" concept implementation. Unlike `ObjectSpace::WeakMap`, it compa
279301

280302
### `Thread::Queue#freeze` and `SizedQueue#freeze` raise `TypeError`
281303

282-
* **Reason:** The discussion was started with a bug report about `Queue` not respecting `#freeze` in any way (`#push` and `#pop` were still working after `#freeze` call). It was then decided that allowing to freeze a queue like any other collection (leaving it immutable) would have questionable semantics: as `Queue` is meant to be an inter-thread communication utility, freezing a queue while some thread waits for it would either leave this thread hanging, or would require `#freeze`'s functionality to extend for communication with dependent threads. Neither is a good option, so the behavior of the method was changed to communicate that queue freezing doesn't make sense.
304+
* **Reason:** The discussion was started with a bug report about `Queue` not respecting `#freeze` in any way (`#push` and `#pop` were still working after `#freeze` call). It was then decided that allowing to freeze a queue like any other collection (leaving it immutable) would have questionable semantics. As `Queue` is meant to be an inter-thread communication utility, freezing a queue while some thread waits for it would either leave this thread hanging, or would require `#freeze`'s functionality to extend for communication with dependent threads. Neither is a good option, so the behavior of the method was changed to communicate that queue freezing doesn't make sense.
283305
* **Discussion:** [Bug #17146]
284306
* **Documentation:** [Thread::Queue#freeze](https://docs.ruby-lang.org/en/master/Thread/Queue.html#method-i-freeze) and [Thread::SizedQueue#freeze](https://docs.ruby-lang.org/en/master/Thread/SizedQueue.html#method-i-freeze)
285307

286308
### `Range#reverse_each`
287309

288310
Specialized `Range#reverse_each` method is implemented.
289311

290-
* **Reason:** Previously, `Range` didn't have a specialized `#reverse_each` method, so calling it would invoke a generic `Enumerable#reverse_each`. The latter works by converting the object to array, and then enumerating this array. In case of a `Range` this can be inefficient (producing large arrays) or impossible (when only upper bound of the range is defined)
312+
* **Reason:** Previously, `Range` didn't have a specialized `#reverse_each` method, so calling it invoked a generic `Enumerable#reverse_each`. The latter works by converting the object to array, and then enumerating this array. In case of a `Range` this can be inefficient (producing large arrays) or impossible (when only upper bound of the range is defined). It also went into infinite loop with endless ranges, trying to enumerate it all to convert into array, while the range can say beforehand that it would be impossible.
291313
* **Discussion:** [Feature #18515]
292314
* **Documentation:** [Range#reverse_each](https://docs.ruby-lang.org/en/master/Range.html#method-i-reverse_each)
293315
* **Code:**
294316
```ruby
317+
# Efficient implementation for integers:
318+
295319
(1..2**100).reverse_each.take(3)
296320
# Ruby 3.2: hangs on my machine, trying to produce an array
297321
# Ruby 3.3: #=> [1267650600228229401496703205376, 1267650600228229401496703205375, 1267650600228229401496703205374]
@@ -301,6 +325,8 @@ Specialized `Range#reverse_each` method is implemented.
301325
# Ruby 3.2: can't iterate from NilClass (TypeError)
302326
# Ruby 3.3: #=> [5, 4, 3]
303327

328+
# Explicit error for endless ranges:
329+
304330
(1...).reverse_each
305331
# Ruby 3.2: hangs forever, trying to produce an array
306332
# Ruby 3.3: `reverse_each': can't iterate from NilClass (TypeError)
@@ -397,31 +423,92 @@ Allows to trace when some exception was `rescue`'d in the code of interest.
397423
```
398424
* **Notes:**
399425
* The discussion was once [started](https://bugs.ruby-lang.org/issues/15973) from the proposal to make `lambda` change "lambiness" of a passed block, but it raises multiple issues (changing the block semantics mid-program is just one of them). In general, `lambda` as a _method_ is considered legacy, inferior to the `-> { }` lambda literal syntax, exactly due to problems like this: it looks like a regular method that receives a block, and therefore should be able accept _any_ block, but in fact it is "special" method. So in 3.0, there was a warning about `lambda(&proc_instance)`, and since 3.3, the warning finally turned into an error.
400-
* There is exactly one occurrence in Ruby where block semantics _changes_ mid-flight:
401426

402427
### Deprecate subprocess creation with method dedicated to files
403428

404-
* **Reason:**
429+
* **Reason:** Methods that are dedicated for opening/reading a file by name historically supported the special syntax of the argument: if it started with pipe character `|`, the subprocess was created and could've been used to communicate with an external command. The functionality is still explained in [Ruby 3.2 docs](https://docs.ruby-lang.org/en/3.2/Kernel.html#method-i-open). It, though, created a security vulnerability: even when the program's author didn't rely on that behavior, the malicious string could've been passed by the attacker instead of an innocent filename.
405430
* **Discussion:** [Feature #19630]
406-
* **Documentation:**
407431
* **Affected methods:**
408-
* Kernel#open
409-
* URI.open
410-
* IO.binread
411-
* IO.foreach
412-
* IO.readlines
413-
* IO.read
414-
* IO.write
432+
* [Kernel#open](https://docs.ruby-lang.org/en/master/Kernel.html#method-i-open)
433+
* [IO.binread](https://docs.ruby-lang.org/en/master/IO.html#method-c-binread)
434+
* [IO.foreach](https://docs.ruby-lang.org/en/master/IO.html#method-c-foreach)
435+
* [IO.readlines](https://docs.ruby-lang.org/en/master/IO.html#method-c-readlines)
436+
* [IO.read](https://docs.ruby-lang.org/en/master/IO.html#method-c-read)
437+
* [IO.write](https://docs.ruby-lang.org/en/master/IO.html#method-c-write)
438+
* [URI.open](https://docs.ruby-lang.org/en/master/URI.html#method-c-open) ([open-uri](https://github.com/ruby/open-uri) standard library)
415439
* **Code:**
440+
```ruby
441+
IO.read('| ls')
442+
#=> contents of the current folder
443+
444+
Warning[:deprecated] = true # Or pass -w command-line option
445+
IO.read('| ls')
446+
# warning: Calling Kernel#open with a leading '|' is deprecated and will be removed in Ruby 4.0; use IO.popen instead
447+
#=> contents of the current folder
448+
```
416449
* **Notes:**
450+
* The documentation for the corresponding methods was adjusted accordingly. Compare the documentation for `Kernel#open` from [3.2](https://docs.ruby-lang.org/en/3.2/Kernel.html#method-i-open) (explains and showcases the `|` trick) and [3.3](https://docs.ruby-lang.org/en/master/Kernel.html#method-i-open) (just mentions that there is a vulnerability to command injection attack).
451+
* As advised by the warning, [IO.popen](https://docs.ruby-lang.org/en/master/IO.html#method-c-popen) is a specialized method when communicating with an external process is desired functionality:
452+
```ruby
453+
IO.popen('ls')
454+
#=> contents of the current folder
455+
```
456+
* As the impact of the change might be big, note that target version for removal is set to **4.0**. To the best of my knowledge, there are no set date for major version yet.
417457

418458
### New `Warning` category: `:performance`
419459

420-
* **Reason:**
460+
A new warning category was introduced for a code that is correct but is known to produces a performance problems. One new such warning was added for objects with too many "shape" variations.
461+
421462
* **Discussion:** [Feature #19538]
422463
* **Documentation:** [Warning#[category]](https://docs.ruby-lang.org/en/master/Warning.html#method-c-5B-5D)
423-
* **Code:**
464+
* **Code:** Here is an example of the new warning in play:
465+
```ruby
466+
class C
467+
def initialize(i)
468+
instance_variable_set("@var_#{i}", i**2)
469+
end
470+
end
471+
472+
Warning[:performance] = true # or pass `-W:performance` command-line argument
473+
474+
(1..10).map { C.new(_1) }
475+
# warning: Maximum shapes variations (8) reached by C, instance variables accesses will be slower.
476+
```
477+
The example is artificial, but it shows the principle: when we have more than 8 instances of the _same_ class, but with different _list of instance variables_ (shape), we might have a performance problem. This means, for example, that a frequently-used class that has many methods with a memoization idiom (`@var ||= value` on the first access) would create the same problem, unless all of them would be initialized in the `initialize`, making all instances having the same shape:
478+
```ruby
479+
class C
480+
# 9 different getters that create an instance varaible
481+
# on the first access.
482+
def var1 = @var1 ||= rand
483+
def var2 = @var2 ||= rand
484+
def var3 = @var3 ||= rand
485+
def var4 = @var4 ||= rand
486+
def var5 = @var5 ||= rand
487+
def var6 = @var6 ||= rand
488+
def var7 = @var7 ||= rand
489+
def var8 = @var8 ||= rand
490+
def var9 = @var9 ||= rand
491+
end
492+
493+
Warning[:performance] = true
494+
# Invoking different getters on different instances of the same class makes
495+
# them have different set of instance variables.
496+
(1..9).map { C.new.send("var#{_1}") }
497+
# warning: Maximum shapes variations (8) reached by C, instance variables accesses will be slower.
498+
499+
# But if we add this to initialize:
500+
class C
501+
def initialize
502+
@var1, @var2, @var3, @var4, @var5, @var5, @var6, @var7, @var8, @var9 = nil
503+
end
504+
end
505+
506+
(1..9).map { C.new.send("var#{_1}") }
507+
# no warning. All objects have the same list of instance vars = the same shape
508+
```
424509
* **Notes:**
510+
* The warning category should be turned on explicitly by providing `-W:performance` CLI option or `Warning[:performance] = true` from the program.
511+
* **Additional reading:** [Performance impact of the memoization idiom on modern Ruby](https://railsatscale.com/2023-10-24-memoization-pattern-and-object-shapes/) by Ruby core team member Jean Boussier.
425512

426513
### `Fiber#kill`
427514

@@ -432,6 +519,7 @@ Terminates the Fiber by sending an exception inside it.
432519
* **Documentation:** [Fiber#kill](https://docs.ruby-lang.org/en/master/Fiber.html#method-i-kill)
433520
* **Code:**
434521
```ruby
522+
TODO: various behaviors
435523
436524
# Semi-realistic usage example:
437525
reader = Fiber.new do
@@ -459,8 +547,6 @@ Terminates the Fiber by sending an exception inside it.
459547
* **Code:**
460548
* **Notes:**
461549
462-
added for checking if two ranges overlap. [[Feature #19839]]
463-
464550
### `Range#overlap?`
465551
466552
Checks for overlapping of two ranges.
@@ -500,6 +586,7 @@ In Ruby 3.3, it will just warn to prepare for a change.
500586
* **Code:** In the code below, where Ruby 3.3 currently produces a warning, Ruby 3.4 would treat `it` as an anonymous block argument; where Ruby 3.3 doesn't produce a warning, Ruby 3.4 would treat `it` as a local variable name or a method call (and would look for such names available in the scope).
501587
```ruby
502588
# The cases that are warned:
589+
# -------------------------
503590
# warning: `it` calls without arguments will refer to the first block param in Ruby 3.4; use it() or self.it
504591

505592
(1..3).map { it } # inside a block without explicit parameters
@@ -508,6 +595,7 @@ In Ruby 3.3, it will just warn to prepare for a change.
508595
(1..3).map { it } # even if a method with name `it` exists in the scope
509596

510597
# The cases that are not warned:
598+
# -----------------------------
511599

512600
it # not inside a block
513601
(1..3).map { |x| it } # inside a block with named parameters

0 commit comments

Comments
 (0)