Project

General

Profile

Actions

Feature #21160

open

Local return from proc

Added by JustJosh (Joshua Stowers) 8 days ago. Updated 7 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:121184]

Description

When writing DSL-style helper methods, I often store block arguments as procs to use as callbacks.
Using return in a proc will return from the context it was created in, which is unsuitable in the following example.
Since procs cannot be converted to lambdas, I end up using next to return a value from them early.

Example:

fulfills_promise :generate_large_image do |image_data|
  next false if image_data.nil?

  puts 'Saving image..'
  # etc.
end

This works but confuses most readers.

I propose introducing an alias for it that is more appropriate for this use case.
Perhaps pass or continue?

It's worth noting that return would work with fulfills_promise :foo, -> (bar) do, though it detracts a bit from a DSL's expressiveness.

Updated by ufuk (Ufuk Kayserilioglu) 8 days ago

next isn't necessarily the correct thing to use here, break is:

foo = fulfills_promise :generate_large_image do |image_data|
  break false if image_data.nil?

  puts 'Saving image..'
  # etc.
end

foo #=> false

And the name exactly conveys the concept of breaking out of the block, in my opinion.

For example:

result = (1..100).each do |num|
  break num if num > 3
  puts num
end

puts "Got result #{result}"

will print

1
2
3
Got result 4

Updated by JustJosh (Joshua Stowers) 8 days ago

ufuk (Ufuk Kayserilioglu) wrote in #note-1:

next isn't necessarily the correct thing to use here, break is:

I've tried break without success - I get LocalJumpError: break from proc-closure

Here is a more complete example of what I am doing:

class Example
  def self.fulfills_promise(promise_name, &block)
    @@promise_callbacks ||= {}
    @@promise_callbacks[promise_name] = block
  end

  def self.fulfill_promise(promise_name, data)
    puts "Fulfilling promise: #{promise_name}"

    callback = @@promise_callbacks[promise_name]

    if callback.call(data)
      puts 'Complete!'
    else
      puts 'Failed!'
    end
  end

  fulfills_promise :generate_large_image do |image_data|
    puts 'Will finalize large image...'
    break false # Indicate something went wrong
    puts 'Finalized!'
    true
  end
end


Example.fulfill_promise(:generate_large_image, 'image data')

Updated by nobu (Nobuyoshi Nakada) 8 days ago ยท Edited

Why not rescue LocalJumpError?

  def self.fulfill_promise(promise_name, data)
    puts "Fulfilling promise: #{promise_name}"

    callback = @@promise_callbacks[promise_name]

    begin
      complete = callback.call(data)
    rescue LocalJumpError => e
      complete = e.exit_value
    end
    if complete
      puts 'Complete!'
    else
      puts 'Failed!'
    end
  end

This works with both of return and break in already released versions of Ruby, and even your DSL does not need to change.
If you want to add new keyword, you'll have to wait until at least the end of the year.

Updated by JustJosh (Joshua Stowers) 7 days ago

nobu (Nobuyoshi Nakada) wrote in #note-3:

Why not rescue LocalJumpError?

That is definitely a better solution than requiring developers to use next.
Thank you for the suggestion.

As I understand it, the reason break doesn't work in my example is because the proc is "orphaned."
Ref: https://docs.ruby-lang.org/en/3.3/Proc.html#class-Proc-label-Orphaned+Proc
If an orphan-friendly implementation or alternative to break is worth considering, I would be happy to take a shot.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0