6

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)
ndnenkov
  • 35,425
  • 9
  • 72
  • 104
Jes
  • 323
  • 2
  • 18
  • 1
    To get you started, you might want to see [this answer about `&:symbol`](http://stackoverflow.com/questions/1961030/ruby-ampersand-colon-shortcut) syntax. – Uzbekjon May 05 '16 at 17:45
  • Possible duplicate of [what happened when pass a method to iterator method](http://stackoverflow.com/questions/13365452/what-happened-when-pass-a-method-to-iterator-method) – Jared Beck May 05 '16 at 17:53
  • What is `translator`? – sawa May 05 '16 at 21:47
  • translator is simply Translator.new – Jes May 06 '16 at 05:53

1 Answers1

7

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".

ndnenkov
  • 35,425
  • 9
  • 72
  • 104
  • Excellent, excellent writeup. One thing: I was under the impression that the &:name syntax actually used symbol#to_proc to create a Proc directly without creating an intermediate block. You say that it "calls the method Translator#speak with :spanish converted to block". Can you clarify? – Michael Gaskill May 05 '16 at 20:15
  • @MichaelGaskill, What happens is `&something` when calling a method will call `something`'s `to_proc` method and then convert that proc to block. This can be verified by creating your own class with `to_proc` method. If the receiving method has `&something` in the signature, this block is converted back to proc. This can be verified by inspecting the `ObjectSpace` and is a cause for slowdown (always use `yield` if you don't really need a reference to the actual proc). – ndnenkov May 05 '16 at 20:28
  • Thank you sir, much appreciated! I just want to make sure I get it right - passing self to the block in this example is just equivalent to invoking language on specific instance of an object? – Jes May 06 '16 at 08:40
  • @Jes, on the same instance (`translator`), yes. – ndnenkov May 06 '16 at 08:43
  • Thank you. One more thing - I thought it is used only with iterators (at least I've seen examples with Enumerable methods such as each and map). So it can be used with any method? – Jes May 06 '16 at 08:49
  • @Jes, yep, any method as seen in the examples above. There are no actual iterators (in the sense you would talk about them in Java/C++ etc.), it's just that methods like `map` pick every individual element in the `Enumerable` and pass it as an argument to the block that was provided. – ndnenkov May 06 '16 at 09:22