Andrew Kwang
Andrew Kwang

Reputation: 19

How does a code block in Ruby know what variable belongs to an aspect of an object?

Consider the following:

(1..10).inject{|memo, n| memo + n}

Question:

How does n know that it is supposed to store all the values from 1..10? I'm confused how Ruby is able to understand that n can automatically be associated with (1..10) right away, and memo is just memo.

I know Ruby code blocks aren't the same as the C or Java code blocks--Ruby code blocks work a bit differently. I'm confused as to how variables that are in between the upright pipes '|' will automatically be assigned to parts of an object. For example:

hash1 = {"a" => 111, "b" => 222}
hash2 = {"b" => 333, "c" => 444}
hash1.merge(hash2) {|key, old, new| old}

How do '|key, old, new|' automatically assign themselves in such a way such that when I type 'old' in the code block, it is automatically aware that 'old' refers to the older hash value? I never assigned 'old' to anything, just declared it. Can someone explain how this works?

Upvotes: 0

Views: 82

Answers (4)

Jörg W Mittag
Jörg W Mittag

Reputation: 369614

A code block is just a function with no name. Like any other function, it can be called multiple times with different arguments. If you have a method

def add(a, b)
  a + b
end

How does add know that sometimes a is 5 and sometimes a is 7?

Enumerable#inject simply calls the function once for each element, passing the element as an argument.

It looks a bit like this:

module Enumerable
  def inject(memo)
    each do |el|
      memo = yield memo, el
    end
    memo
  end
end

Upvotes: 2

ForeverZer0
ForeverZer0

Reputation: 2496

Just to simplify some of the other good answers here:

If you are struggling understanding blocks, an easy way to think of them is as a primitive and temporary method that you are creating and executing in place, and the values between the pipe characters |memo| is simply the argument signature.

There is no special special concept behind the arguments, they are simply there for the method you are invoking to pass a variable to, like calling any other method with an argument. Similar to a method, the arguments are "local" variables within the scope of the block (there are some nuances to this depending on the syntax you use to call the block, but I digress, that is another matter).

The method you pass the block to simply invokes this "temporary method" and passes the arguments to it that it is designed to do. Just like calling a method normally, with some slight differences, such as there are no "required" arguments. If you do not define any arguments to receive, it will happily just not pass them instead of raising an ArgumentError. Likewise, if you define too many arguments for the block to receive, they will simply be nil within the block, no errors for not being defined.

Upvotes: 1

max pleaner
max pleaner

Reputation: 26788

The parameters for the block are determined by the method definition. The definition for reduce/inject is overloaded (docs) and defined in C, but if you wanted to define it, you could do it like so (note, this doesn't cover all the overloaded cases for the actual reduce definition):

module Enumerable
  def my_reduce(memo=nil, &blk)
    # if a starting memo is not given, it defaults to the first element
    # in the list and that element is skipped for iteration
    elements = memo ? self : self[1..-1]
    memo ||= self[0]
    elements.each { |element| memo = blk.call(memo, element) }
    memo
  end
end

This method definition determines what values to use for memo and element and calls the blk variable (a block passed to the method) with them in a specific order.

Note, however, that blocks are not like regular methods, because they don't check the number of arguments. For example: (note, this example shows the usage of yield which is another way to pass a block parameter)

def foo
  yield 1
end

# The b and c variables here will be nil
foo { |a, b, c| [a,b,c].compact.sum } # => 1

You can also use deconstruction to define variables at the time you run the block, for example if you wanted to reduce over a hash you could do something like this:

# this just copies the hash
{a: 1}.reduce({}) { |memo, (key, val)| memo[key] = val; memo }

How this works is, calling reduce on a hash implicitly calls to_a, which converts it to a list of tuples (e.g. {a: 1}.to_a = [[:a, 1]]). reduce passes each tuple as the second argument to the block. In the place where the block is called, the tuple is deconstructed into separate key and value variables.

Upvotes: 2

Sergio Tulentsev
Sergio Tulentsev

Reputation: 230551

And memo is just memo

what do you mean, "just memo"? memo and n take whatever values inject passes. And it is implemented to pass accumulator/memo as first argument and current collection element as second argument.

How do '|key, old, new|' automatically assign themselves

They don't "assign themselves". merge assigns them. Or rather, passes those values (key, old value, new value) in that order as block parameters.

If you instead write

hash1.merge(hash2) {|foo, bar, baz| bar}

It'll still work exactly as before. Parameter names mean nothing [here]. It's actual values that matter.

Upvotes: 1

Related Questions