Search code examples
rubyproc

Ruby Proc.new in method with implicit block no longer supported?


This is a small excerpt from Matz's book:

If Proc.new is invoked without a block from within a method that does have an asso- ciated block, then it returns a proc representing the block associated with the containing method. Using Proc.new in this way provides an alternative to using an ampersand-prefixed block argument in a method definition. The following two methods are equivalent, for example:

def invoke(&b) 
  b.call
end

def invoke 
    Proc.new.call
end

This has been my understanding. However, after upgrading to Ruby v. 3.1.2, it seems that it is no longer the case that the methods are equivalent. Now, if I invoke both of these methods, so:

def invoke(&b) 
  b.call
end

def invoke2 
  Proc.new.call
end

invoke {puts 'hello'}
invoke2 {puts 'hello'}

hello
test.rb:6:in `new': tried to create Proc object without a block (ArgumentError)

I've looked around for why, and found some interesting information here:
Can a Ruby method access the implicit block argument? In this, Stefan's answer quotes the doc from v. 2.7.1 thus:

::new may be called without a block only within a method with an attached block, in which case that block is converted to the Proc object.

However, this bit of prose has mysteriously disappeared from the 3.1.2 doc.

Jörg W Mittag has an interesting take in his answer:

[This] was actually an unintended side-effect of how Proc::new was implemented in MRI: Proc::new did not check whether you passed a block or not, it simply assumed that you passed a block and would take the first block off the top of the internal VM stack. So, if you didn't pass a block to Proc::new, it would actually end up creating a Proc for the implicit block that was passed to the method (since that was the one which just happened to be on the top of the stack).

But, that was never portable, never guaranteed, never worked in all Ruby implementations, and AFAIK no longer works in YARV.

This information suggests that the feature could have disappeared at any time.

Digging through the documentation archives, I find that the last version that references being able to do this is 3.0.5. The next version, 3.1.0, drops the quoted paragraph with no fanfare.

What I'm looking for is some sort of definitive statement somewhere that this use of Proc.new in a method without an explicit block argument is no longer supported as of version such and so. Does anyone have such?

My broader concern is that I have thought of Matz's book as a source of truth, and now I find that in one small part it is obsolete. Does anyone have any further instances of obsolescence in Matz's book, and is there some sort of documentation somewhere that lists all the feature changes from version to version?


Solution

  • TL;DR

    With credit to @Stefan for the bug tracker link, and to other commenters for various explanations, the feature you're referring to had a ticket opened for deprecation and removal over 8 years ago.

    Detailed Explanation with Links and Citations

    So, some of the reasons Ruby doesn't currently do what you expect and why the original Ruby book doesn't match all Ruby implementations include:

    1. Alternative implementations, using different compilers, languages, runtimes, and virtual machines is a moving target and a lot of the current Ruby ecosystem aren't covered in the original book. A lot has changed since then. More on this below.

    2. The (mis-)feature was removed in Ruby 3.0.0, but the documentation wasn't updated to match until this commit was merged into proc.c in Ruby v3_0_0_rc2-166-g8da7f4abc7.

    3. The book you're referring to was written and published at least 15 years ago. The citation for the book, which is still available on Amazon and from O'Reilly is:

      The Ruby Programming Language: Everything You Need to Know, 1st Edition. Flanagan, David and Yukihiro Matsumoto. O'Reilly Media; January 2008.

    4. While there's still a lot of good material in the book you're referencing from a language design perspective, a lot has changed in Ruby since then so it should no longer be considered a canonical source for how the Ruby interpreter or virtual machine implements various internals.

    5. There are some other books that focus on Ruby internals, but as far as I know none of them are really up to date for Ruby >= 3.2.2.

    6. It is possible that the forthcoming book from Pragmatic Programmers entitled Programming Ruby 3.2, 5th Ed. by Noel Rappin et al. will be more up to date, but the outline of the book in beta suggests that it isn't spilling a lot of ink over internals but is likely to cover notable changes.

      NB: Whether or not this particular use case will be notable enough for the appendix currently titled "Ruby Changes" is not something I can guess at, but since the book is in beta you could always participate in the beta and suggest its inclusion if you feel it's important.

    7. By definition, the canonical source for how the Ruby language is implemented is the source code on GitHub for the reference implementation with C and YARV. Other Ruby immplementations such as TruffleRuby or JRuby may implement things differently or not at all.

    8. The reference implementation maintains an issue tracker which can be useful for discovering discussions about past, pending, or rejected feature changes, but is not necessarily canonical in the same way the actual source code is when it comes to how code is implemented, although it should certainly point to commits that affect them.

    9. The official documentation for the reference implementations as far back as 2.0.0 are available on https://docs.ruby-lang.org/en/, but many people also rely on https://ruby-doc.org/. Currently, the latter includes information about point releases while the former does not. YMMV.

    10. Continue to use the older book as a reference about how Ruby was designed, but please migrate to newer books and canonical sources for questions about specific implementation issues. This is especially important if you're talking about uncommon usages, edge cases, alternative implementations, or platform-specific issues.