Skip to content

Instantly share code, notes, and snippets.

@jmtame
Last active March 26, 2018 07:49
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save jmtame/6458832 to your computer and use it in GitHub Desktop.
Save jmtame/6458832 to your computer and use it in GitHub Desktop.
ruby blocks and yield explained by a bloc mentor

The following is an explanation of Ruby blocks and yield by another Bloc mentor (Adam Louis) who was trying to explain it to one of his students.

On my very first day programming, if someone asked me for "the sum of the numbers from 1 to 10", I'd have written:

puts 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10

Easy enough.

But if they then asked me for "the sum the numbers from 1 to 100", I'd have written:

puts 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + ... That would be a hell of a lot of typing. We wrote the exact same sequence of +s over and over for every number.

Then we learned about Loops, and if someone asked me for "the sum of the numbers from 1 to 100", I'd have written:

sum = 0
(1..100).each do |num|
  sum += num
end
puts sum

We had learned to abstract the logic, so that it worked with as many numbers as we wanted, and we only had to write + once.

But if they then asked me for "the sum of the numbers from 1 to 1000", I'd have written:

sum = 0
(1..1000).each do |num|
  sum += num
end
puts sum

We wrote nearly the exact same summing loop again, just using a different list of numbers.

Then we learned about Methods, and if someone asked me for "the sum of the numbers from 1 to 100, and then 1 to 1000", I'd have written:

def sum(array)
  sum = 0
  array.each do |num|
    sum += num
  end
  sum
end

puts sum(1..100)
puts sum(1..1000)

We had learned to abstract the logic, so that it worked with any list of numbers we wanted, and we only has to write the summing loop once.

But if they then asked me for "the product of the numbers from 1 to 1000", I'd have written:

def product(array)
  product = 1
  array.each do |num|
    product *= num
  end
  product
end

puts product(1..1000)

We wrote nearly the exact same looping code again, just using a different starting number and a different method to combine the numbers in the list.

#Aside: Blocks and yield

At this point we can abstract our logic so that we can write code once that takes and uses different values, like swapping (1..100) for (1..1000) as the list we're giving to sum.

But to abstract our logic any further, we need a way to write code once that takes and uses different code. Like swapping + for *.

One thing to know going forward:

In:

array.each do |num|
  sum += num
end

the code:

do |num|
  sum += num
end

is called a Block. It's like a chunk of code that can be spliced in to any method set up to receive it (which only requires the use of the keyword yield). The method receiving a Block can interrupt its own execution and yield to the Block, which runs on its own and can even take arguments and return a value, as if it were just a method without a name.

Meta-Note: teach explicit Proc-passing first, then introduce yield as an alias to an unnamed Block? Next we learned about Blocks, and if someone asked me for "the sum and the product of the numbers from 1 to 1000", I'd have written:

# "inject" an operation in-between the values in a list
# eg: injecting `+` into `[1, 2, 3]` gives `1 + 2 + 3`
def inject(initial, array)
  result = initial
  array.each do |x|
    result = yield(result, x)
  end
  result
end

def sum(array)
  inject(0, array) do |x,y|
    x + y
  end
end

def product(array)
  inject(1, array) do |x,y|
    x * y
  end
end

puts sum(1..1000)
puts product(1..1000)

We had learned to abstract the logic, so that it worked with any starting number and any method to combine the numbers in the list, and we only had to write the looping code once.

But if they then asked me for "the alphabet with spaces between the letters", I'd have written:

def join(separator, array)
  inject("", array) do |x,y|
    x + separator + y
  end
end

puts join(" ", 'a'..'z')

... and it just works! We not only generalized sum to share code with product, we now have a method so general that it works with things we didn't even design it for. That's fucking cool. And inject just an example, barely the tip of the Blocks iceberg.

#Re-implementing Array#collect

class Array
  def new_collect
    results = []
    0.upto(self.length - 1) do |index|
      results << yield(self[index])
    end
    results
  end
end

index is used to get the element at each index in the self array. We can walk through what the code kind of looks like as it runs:

You ask Ruby to evaluate:

[9,8,7].new_collect { |x| x + 1 }

Ruby jumps into Array #new_collect. While running this method, Ruby has the following values in the "local environment":

self = [9,8,7]

def yield(x)
  x + 1
end

It runs the first expression:

results = []

Now the enviroment looks like:

self = [9,8,7]

def yield(x)
  x + 1
end

results = []

Next, Ruby starts to calculate the next expression -- upto and the do ... end block. First it reduces all the stuff it needs to pass a definite value to upto:

0.upto(self.length - 1) do |index|
0.upto([9,8,7].length - 1) do |index|
0.upto(3 - 1) do |index|
0.upto(2) do |index|

Then, as described by the code inside the Integer #upto method, it runs the do block once for each number between 0 and 2:

do |index|
  results << yield(self[index])
end
...
do |0|
  results << yield(self[0])
end

  results << yield([9,8,7][0])
  results << yield(9)
  results << 9 + 1
  results << 10
  results
  [10]
...
do |1|
  results << yield(self[1])
end

  results << yield([9,8,7][1])
  results << yield(8)
  results << 8 + 1
  results << 9
  results
  [10, 9]
...
do |2|
  results << yield(self[2])
end

  results << yield([9,8,7][1])
  results << yield(7)
  results << 7 + 1
  results << 8
  results
  [10, 9, 8]

Now the environment looks like:

self = [9,8,7]

def yield(x)
  x + 1
end

results = [10, 9, 8]

Then Ruby runs the last expression:

results

and returns:

[10, 9, 8]

So, back up at the level where this method was called, the whole of:

[9,8,7].new_collect { |x| x + 1 }

is reduced to:

[10, 9, 8]

And the program goes on :)

However, you don't need to keep track of index yourself. The upto code is just copied over from the example Array #each implementation. each already abstracts out the logic of getting each element from the array.

You have a tool to get every element of the array, and you can build further tools like collect/map using it. That's the whole point of blocks: you can reuse the same code "structure" but give it different logic to use inside that "structure", so you don't need to repeat the pattern manually every time. Using each lets you abstract out the pattern of getting every element using its index. And you can use it to abstract out the logic of doing an operation on each element and collecting the results, AKA collect/map:

class Array
  def new_collect
    results = []
    self.each do |element|
      results << element
    end
    results
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment