Skip to content

Commit 705bc6e

Browse files
committed
Advent of Changelog: Day 10
1 parent 09e8cbb commit 705bc6e

File tree

1 file changed

+180
-19
lines changed

1 file changed

+180
-19
lines changed

_src/3.3.md

Lines changed: 180 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,81 @@ description: Ruby 3.3 full and annotated changelog
2222

2323
### `Array#pack` and `String#unpack`: raise `ArgumentError` for unknown directives
2424

25-
* **Reason:**
2625
* **Discussion:** [Bug #19150]
2726
* **Documentation:** [doc/packed_data.rdoc](https://docs.ruby-lang.org/en/master/packed_data_rdoc.html)
2827
* **Code:**
28+
```ruby
29+
[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)
33+
```
2934
* **Notes:**
3035

3136
### `Dir.for_fd` and `Dir.fchdir`
3237

33-
* **Reason:**
38+
Two methods to accept an integer file descriptor as an argument: `for_fd` creates a `Dir` object from it; `fchdir` changes the current directory to one specified by a descriptor.
39+
40+
* **Reason:** New methods allow to use UNIX file descriptors if they are returned from a C-level code or obtained from OS.
3441
* **Discussion:** [Feature #19347]
3542
* **Documentation:** [Dir.for_fd](https://docs.ruby-lang.org/en/master/Dir.html#method-c-for_fd), [Dir.fchdir](https://docs.ruby-lang.org/en/master/Dir.html#method-c-fchdir)
3643
* **Code:**
44+
```ruby
45+
fileno = Dir.new('doc/').fileno
46+
# In reality, this #fileno might come from other library
47+
48+
dir = Dir.for_fd(fileno)
49+
#=> #<Dir:0x00007f8831b810a8> -- no readable path representation
50+
dir.path #=> nil
51+
dir.to_a
52+
#=> ["forwardable.rd.ja", "packed_data.rdoc", "marshal.rdoc", "format_specifications.rdoc", ....
53+
# It was performed in the Ruby's core folder, and lists the doc/ contents
54+
55+
# Attempt to use a bogus fileno will result in error:
56+
Dir.for_fd(0)
57+
# `for_fd': Not a directory - fdopendir (Errno::ENOTDIR)
58+
59+
# Same with fileno that doesn't designate a directory:
60+
Dir.for_fd(Dir.new('README.md').fileno)
61+
# in `initialize': Not a directory @ dir_initialize - README.md (Errno::ENOTDIR)
62+
63+
# Same logic works for .fchdir
64+
Dir.fchdir(fileno) #=> 0
65+
Dir.pwd
66+
# "/home/zverok/projects/ruby/doc" -- the current path have changed successfully
67+
```
3768
* **Notes:**
69+
* The functionality is only supported on POSIX platforms;
70+
* The initial [ticket](https://bugs.ruby-lang.org/issues/19347) only proposed to find a way to be able to change a current directory to one specified by a descriptor (i.e., what eventually became `.fchdir`), but during the discussion a need were discovered for a generic instantiation of a `Dir` instance from the descriptor (what became `from_fd`), as well as a generic way to change the current directory to one specified by `Dir` instance ([`#chdir`](TODO), which is not related to descriptors but is generically useful).
3871

3972
### `Dir#chdir`
4073

41-
* **Reason:**
74+
An instance method version of [Dir.chdir](https://docs.ruby-lang.org/en/master/Dir.html#method-c-chdir): changes the current working directory to one specified by the `Dir` instance.
75+
4276
* **Discussion:** [Feature #19347]
4377
* **Documentation:** [Dir#chdir](https://docs.ruby-lang.org/en/master/Dir.html#method-i-chdir)
4478
* **Code:**
45-
* **Notes:**
79+
```ruby
80+
Dir.pwd #=> "/home/zverok/projects/ruby"
81+
dir = Dir.new('doc')
82+
dir.chdir #=> nil
83+
Dir.pwd #=> "/home/zverok/projects/ruby/doc"
84+
```
85+
* **Notes:** Unlike [Dir.chdir](https://docs.ruby-lang.org/en/master/Dir.html#method-c-chdir), the new method doesn't have a block form (the form that restores previous working directory after the block is finished).
4686

4787
### `MatchData#named_captures`: `symbolize_names:` argument
4888

49-
* **Reason:**
5089
* **Discussion:** [Feature #19591]
5190
* **Documentation:** [MatchData#named_captures](https://docs.ruby-lang.org/en/master/MatchData.html#method-i-named_captures)
5291
* **Code:**
53-
* **Notes:**
92+
```ruby
93+
m = "2023-12-25".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
94+
m.named_captures
95+
#=> {"year"=>"2023", "month"=>"12", "day"=>"25"}
96+
m.named_captures(symbolize_names: true)
97+
#=> {:year=>"2023", :month=>"12", :day=>"25"}
98+
```
99+
* **Notes:** While `symbolize_names:` might looks somewhat strange (usually we talk about hash _keys_), it is done for consistency with Ruby standard library's [`JSON.parse`](https://docs.ruby-lang.org/en/master/JSON.html#module-JSON-label-Output+Options) signature, which inherited the terminology from the JSON specification.
54100

55101
### `Module#set_temporary_name`
56102

@@ -60,7 +106,80 @@ Allows to assign a string to be rendered as class/module's `#name`, without assi
60106
* **Discussion:** [Feature #19521]
61107
* **Documentation:** [Module#set_temporary_name](https://docs.ruby-lang.org/en/master/Module.html#method-i-set_temporary_name)
62108
* **Code:**
63-
* **Notes:**
109+
```ruby
110+
dynamic_class = Class.new do
111+
def foo; end
112+
end
113+
114+
dynamic_class.name #=> nil
115+
116+
# For dynamic classes, representation of related values is frequently unreadable:
117+
dynamic_class #=> #<Class:0x0...>
118+
instance = dynamic_class.new #=> #<#<Class:0x0...>:0x0...>
119+
instance.method(:foo) #=> #<Method: #<Class:0x0...>#foo() ...>
120+
121+
dynamic_class::Nested = Module.new
122+
dynamic_class::Nested #=> #<Class:0x0...>::Nested
123+
124+
# After assigning the temporary name, representation becomes more convenient:
125+
dynamic_class.set_temporary_name("MyDSLClass(with description)")
126+
127+
dynamic_class #=> MyDSLClass(with description)
128+
instance #=> #<MyDSLClass(with description):0x0...>
129+
instance.method(:foo) #=> #<Method: MyDSLClass(with description)#foo() ...>
130+
131+
# Note that module constant names are assigned at the moment of their creation,
132+
# and don't change when the temporary name is assigned:
133+
dynamic_class::OtherNested = Module.new
134+
135+
dynamic_class::Nested #=> #<Class:0x0...>::Nested
136+
dynamic_class::OtherNested #=> MyDSLClass(with description)::OtherNested
137+
138+
# Assigning names that correspond to constant name rules is prohibited:
139+
dynamic_class.set_temporary_name("MyClass")
140+
# `set_temporary_name': the temporary name must not be a constant path to avoid confusion (ArgumentError)
141+
dynamic_class.set_temporary_name("MyClass::NestedName")
142+
# `set_temporary_name': the temporary name must not be a constant path to avoid confusion (ArgumentError)
143+
144+
# When the module with a temporary name is put into a constant,
145+
# it receives a permanent name, which can't be changed anymore
146+
C = dynamic_class
147+
148+
# It affects all associated values (including modules)
149+
150+
dynamic_class #=> C
151+
instance #=> #<C:0x0...>
152+
instance.method(:foo) #=> #<Method: C#foo() ...>
153+
dynamic_class::Nested #=> C::Nested
154+
dynamic_class::OtherNested #=> C::OtherNested
155+
156+
dynamic_class.set_temporary_name("Can I have it back?")
157+
# `set_temporary_name': can't change permanent name (RuntimeError)
158+
159+
# `nil` can be used to cleanup a temporary name:
160+
other_class = Class.new
161+
other_class.set_temporary_name("another one")
162+
other_class #=> another one
163+
other_class.set_temporary_name(nil)
164+
other_class #=> #<Class:0x0...>
165+
```
166+
* **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:
167+
```ruby
168+
it "works as a calculator" do
169+
expec(2+2).to eq 4
170+
end
171+
# If we assign just the example description as a temp.name, the
172+
# error would look like this:
173+
#
174+
# undefined method `expec' for an instance of works as a calculator
175+
# ^^^^^^^^^^^^^^^^^^^^^
176+
#
177+
# ...which is confusing. So it is probably better to construct a
178+
# module-like temporary name, to have:
179+
#
180+
# undefined method `expec' for an instance of MyFramework::Example("works as a calculator")
181+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
182+
```
64183

65184
### `ObjectSpace::WeakKeyMap`
66185

@@ -100,13 +219,37 @@ A new "weak map" concept implementation. Unlike `ObjectSpace::WeakMap`, it compa
100219
```
101220
* **Notes:**
102221

103-
### `Proc#dup` and `#clone` call `#initialize_dup` and `#initialize_clone`
222+
### `Proc#dup` and `#clone` call `#initialize_dup` and `#initialize_copy`
104223

105-
* **Reason:**
224+
* **Reason:** A fix for an old inconsistency: `Object`'s `#dup` and `#clone` methods docs
106225
* **Discussion:** [Feature #19362]
107226
* **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))
108227
* **Code:**
109-
* **Notes:**
228+
```ruby
229+
# The examples would work the same with
230+
# #dup/#initialize_dup and #clone/#initialize_copy
231+
class TaggedProc < Proc
232+
attr_reader :tag
233+
234+
def initialize(tag)
235+
super()
236+
@tag = tag
237+
end
238+
239+
def initialize_dup(other)
240+
@tag = other.tag
241+
super
242+
end
243+
end
244+
245+
proc = TaggedProc.new('admin') { }
246+
247+
proc.tag #=> 'admin'
248+
proc.dup.tag
249+
# Ruby 3.2 => nil, the duplication didn't went through initialize_dup
250+
# Ruby 3.3 => "admin"
251+
```
252+
* **Notes:** Inheriting from core classes is an advanced technique, and most of the times there are simple ways to achieve same goals (like wrapper objects containing a `Proc` and an additional info).
110253

111254
### `Process.warmup`
112255

@@ -118,27 +261,45 @@ A new "weak map" concept implementation. Unlike `ObjectSpace::WeakMap`, it compa
118261

119262
### `Process::Status#&` and `#>>` are deprecated
120263

121-
* **Reason:**
264+
* **Reason:** These methods have been treating `Process::Status` as a very thin wrapper around an integer value of the return status of the process; which is unreasonable for supporting Ruby in more varying environments.
122265
* **Discussion:** [Bug #19868]
123266
* **Documentation:** [Process::Status#&](https://docs.ruby-lang.org/en/master/Process/Status.html#method-i-26), [#>>](https://docs.ruby-lang.org/en/master/Process/Status.html#method-i-3E-3E)
124267
* **Code:**
125-
* **Notes:**
126268

127269
### `Thread::Queue#freeze` and `SizedQueue#freeze` raise `TypeError`
128270

129-
* **Reason:**
271+
* **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.
130272
* **Discussion:** [Bug #17146]
131273
* **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)
132-
* **Code:**
133-
* **Notes:**
134274

135-
### `Range#reverse_each` behavior change with semi-open ranges
275+
### `Range#reverse_each`
136276

137-
* **Reason:**
138-
* **Discussion:** [Feature #18515], [Feature #18551]
277+
Specialized `Range#reverse_each` method is implemented.
278+
279+
* **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)
280+
* **Discussion:** [Feature #18515]
139281
* **Documentation:** [Range#reverse_each](https://docs.ruby-lang.org/en/master/Range.html#method-i-reverse_each)
140282
* **Code:**
141-
* **Notes:**
283+
```ruby
284+
(1..2**100).reverse_each.take(3)
285+
# Ruby 3.2: hangs on my machine, trying to produce an array
286+
# Ruby 3.3: #=> [1267650600228229401496703205376, 1267650600228229401496703205375, 1267650600228229401496703205374]
287+
# (returns immediately)
288+
289+
(...5).reverse_each.take(3)
290+
# Ruby 3.2: can't iterate from NilClass (TypeError)
291+
# Ruby 3.3: #=> [5, 4, 3]
292+
293+
(1...).reverse_each
294+
# Ruby 3.2: hangs forever, trying to produce an array
295+
# Ruby 3.3: `reverse_each': can't iterate from NilClass (TypeError)
296+
297+
# The latter change affects any type of range beginning:
298+
('a'...).reverse_each
299+
# Ruby 3.2: hangs forever, trying to produce an array
300+
# Ruby 3.3: `reverse_each': can't iterate from NilClass (TypeError)
301+
```
302+
* **Notes:** Other than raising `TypeError` for endless ranges (which works with any type of range beginning), the specialized behavior is only implemented for `Integer`. A possibility of a generalization was [discussed](https://bugs.ruby-lang.org/issues/18515#note-4) by using object's `#pred` method (opposite to `#succ`, which the range uses to iterate forward), but the scope of this change would be bigger, as currently only `Integer` implements such method. It is possible that the adjustments would be made in the future versions.
142303

143304
### `Refinement#target` as an alternative of `Refinement#refined_class`
144305

0 commit comments

Comments
 (0)