Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unary Ampersand Operator and passing procs as arguments in Ruby

Tags:

ruby

I'm having trouble understanding this code below.

I get the idea of Unary Ampersand Operator and passing procs as arguments to methods. But I really can't wrap my head around passing self to the language.call. I understand it like this: we're passing self as an argument to the proc/block language. It doesn't make any sense to me. Can someone please explain? :)

class Translator    
  def speak &language
    language.call(self)    
  end

  protected

  def french
    'bon jour'  
  end

  def spanish
    'hola'   
  end

  def turkey
    'gobble'   
  end

  def method_missing(*args)
    'awkward silence'
  end 
end

We're using it with:

translator.speak(&:spanish)
like image 794
Jes Avatar asked May 05 '16 17:05

Jes


1 Answers

This example beautifully ties together multiple Ruby concepts. Because of that, I will try to explain all of them.

  1. Blocks

Methods in Ruby can accept blocks (pieces of code) in elegant matter:

def run_code
  yield
end

run_code { puts 42 } # => prints 42
  1. Procs are similar to blocks, but they are actual addressable objects:
deep_thought = proc { puts 42 }
deep_thought.call # => prints 42
  1. You can turn a proc into a block when calling a method with the & operator:
def run_code
  yield
end

deep_thought = proc { puts 42 }
run_code(&deep_thought) # => prints 42
  1. Procs and blocks can accept arguments:
def reveal_answer
  yield 5_000
end

deep_thought = proc do |years_elapsed|
  years_elapsed >= 7_500_000 ? 42 : 'Still processing'
end

reveal_answer(&deep_thought) # => 'Still processing'
  1. You can turn a block into proc using & in the method signature:
def inspector(&block)
  puts block.is_a?(Proc)
  puts block.call
end

inspector { puts 42 } # => prints true and 42
inspector(&proc { puts 42 }) # => the same
  1. Symbol#to_proc creates a proc that calls methods with the same name on the object:
class Dummy
  def talk
    'wooooot'
  end
end

:talk.to_proc.call(Dummy.new) # => "wooooot"

In other words,

:bar.to_proc.call(foo)

is pretty much equivalent to

foo.bar
  1. BasicObject#method_missing:

When you try to call a method on an object, Ruby traverses it's ancestor chain, searching for a method with that name. How the chain is constructed is a different topic, lengthy enough for another day, the important thing is that if the method is not found til the very bottom (BasicObject), a second search is performed on the same chain - this time for a method called method_missing. It gets passed as arguments the name of the original method plus any argument it received:

class MindlessParrot
  def method_missing(method_name, *args)
    "You caldt #{method_name} with #{args} on me, argh!"
  end
end

MindlessParrot.new.foo          # => "You caldt foo with [] on me, argh!"
MindlessParrot.new.bar :baz, 42 # => "You caldt bar with [:baz, 42] on me, argh!"

So what does all this mean in our specific case? Lets assume for a second there was no protected.


translator.speak(&:spanish)

calls the method Translator#speak with :spanish converted to block.


Translator#speak takes that block and transforms it to a proc, named language, and calls it, passing self as argument.


self is an instance of Translator, therefore, it has the methods speak, french, spanish, turkey and method_missing.


And so:

Translator.new.speak(&:spanish)

is equivalent to:

:spanish.to_proc.call(Translator.new)

which is equivalent to:

Translator.new.spanish

giving us "hola".


Now, taking the protected back, all the language methods of our translator object are still present, but they can not be directly accessed by outsiders.


Just as you can't call

Translator.new.spanish

and expect "hola" back, you can't call

Translator.new.speak(&:spanish)




And since the method is not directly accessible, it is considered not found and method_missing is called, thus giving us "awkward silence".

like image 161
ndnenkov Avatar answered Sep 20 '22 20:09

ndnenkov