Home > database >  Ruby - providing multiple lambda or proc arguments to a function?
Ruby - providing multiple lambda or proc arguments to a function?

Time:11-06

I've implemented a rudimentary extension on Hash with delegation via the forwardable module, in the following source code (public domain here)

module OptionsConstants
  THIS = lambda { |a| a.itself }
end


require('forwardable')

class AssocHash

  extend(Forwardable)
  include(Enumerable)
  def_delegators(:@table,
                 :[], :delete, :each, :keys, :values,
                 :length, :empty?,
                 :include?, :has_key?, :key?, :member?
                )

  def self.table_default(whence)
    return lambda { |hash, key| raise("Found no key #{key} in #{whence.class} #{whence.object_id}") }
  end

  def initialize(&keytest)
    @table = Hash.new(&self.class.table_default(self))
    @keytest = ( keytest || OptionsConstants::THIS )
  end
  

  def add(obj, overwrite = false)
    usekey=key(obj)
    if @table.member?(usekey) 
      if (obj == @table[usekey])
        return obj
      elsif ! overwrite
        raise("An object is already registered for key #{usekey} in #{self.class} #{self}")
      end
    end
    @table[usekey]=obj
    return obj
  end

  def get(key)
    return @table[key]
  end

  def key(obj)
    @keytest.call(obj)
  end

end

I'm still studying the syntax and semantics of the Ruby language. I think I've read a little about how & might be handled in method argument lists, in the documentation for the Proc class in Ruby 2.6. I'm not sure if I'm entirely clear about how this &procarg syntax may operate, however. Perhaps it's nothing like a pointer in C?

For implementing the AsssocHash class, I'd like to be able to pass two lambda objects to the initialize method, ideally with the second being optional, such that the second lambda object (or general proc) would be then used to provide a 'default' proc for the encapsulated hash @table. It would need to be provided as a proc to the Hash initializer. Subsequently, the default proc could then be provided from any calling API, in addition to the @keytest proc used directly in AssocHash.

While working on the API for this, locally, it seemed that there may be some limitations entailed when using a &procarg syntax?

  • Cannot use two &procarg in a method signature?
  • Cannot use any arg after a &procarg in a method signature?
  • Does not provide a conventional required parameter, in an argument list?

Presently, I may only be able to guess as to how & might be used when passing any object/lambda/proc to another method. While it may have "Just worked" to add & to the expression for providing a default proc for the Hash initializer in the present implementation, in all candor I'm not certain as to why it was needed there, or how it affects what the Hash initializer receives. This might seem tangential to the question of how to provide any two proc expressions to the top-level initializer, in this code?

I'm not certain if it's the most accurate terminology, to refer to it as a &procarg. To my own albeit limited understanding of this aspect of Ruby syntax, the & syntax seems to be required for the parameter that provides the value of @keytest to the initializer for AssocHash, such that the @keytest can then be used later in the expression, @keytest.call

Is there some way to provide a second proc to the initializer method, such that then could be used as a proc default for the Hash initializer?

I'll try out a few more iterations on the syntax, here. I'm afraid there may not seem to be a lot of documentation around, about this & qualifier in Ruby method signatures. I'm sure that it's thoroughly documented in the source code, however.

Update

Perhaps & may not be required for passing a lambda or proc to a method?

I believe that the following code may serve towards addressing the question

module TestConstants
  NOKEY = lambda { |h,k| return "No key: #{k}" }
end

class Test
  def mkhash(fn = TestConstants::NOKEY) 
    Hash.new(&fn)
  end
end

subsequently, under irb:

irb(main)> t = Test.new
=> #<Test:0x0000557c6fe2e460>

irb(main)> h = t.mkhash
=> {}

irb(main)> h[:a]
=> "No key: a"

irb(main)> h = t.mkhash(lambda { |h,k| return "Not found: #{k}" })
=> {}

irb(main)> h[:b]
=> "Not found: b"

If it's possible to pass a lambda or proc to a method without denoting the parameter for it with &, then the earlier AssocHash code can probably be updated as to how each lambda value will be passed in to the method.

Perhaps that would be independent to how each lambda would then be stored within any instance variables and/or passed to the Hash constructor.

At least, it might serve towards addressing a question of how to revise that single class. This & qualifier is certainly an interesting bit of syntax, imo.

CodePudding user response:

Methods in Ruby may not take more than one block. However, you can bind any arbitrary number of blocks as procs (or lambdas) and pass them as regular arguments. You don't get the nice block sugar syntax (do...end), but it works just fine.

In Ruby, blocks (the anonymous functions passed to methods via {|...| } or do ... end syntax) are one of the very few exceptions to the "everything is an object" rule. Ruby makes special allowances for the fact that it is not uncommon to want to pass a callback or anonymous function to a method. You pass that with the block syntax, then can declare that you want access to the block as a Proc by adding the &block parameter as the last argument in your arg list, which takes your passed anonymous function and binds it to a Proc instance stored in the variable block. This lets you call it (by calling block.call) or pass it on as a normal object instance (as block) or as a block (as &block). You can also implicitly call it with yield without binding it to a Proc.

def debug(obj = nil, &block)
  p [:object, obj]
  p [:block, block]
end

def pass_block_as_proc(&foo)
  puts "Passing as foo"
  debug(foo)
  puts "Passing as &foo"
  debug(&foo)
end

pass_block_as_proc {}

Output:

Passing as foo
[:object, #<Proc:0x000055ee3a724a38>]
[:block, nil]
Passing as &foo
[:object, nil]
[:block, #<Proc:0x000055ee3a724a38>]

So, if we want to pass multiple callbacks to a method, we can't rely on the anonymous block sugar. Instead, we have to pass them as bound Proc instances:

def multiple_procs(value, proc1, proc2)
  proc2.call(proc1.call(value))
end

puts multiple_procs "foo",
      ->(s) { s.upcase },
      ->(s) { s   s }

# => FOOFOO

This link is some additional useful reading to understand the difference between blocks and procs.

  • Related