Skip to content

Instantly share code, notes, and snippets.

@PamBWillenz
Last active August 29, 2015 14:19
Show Gist options
  • Save PamBWillenz/8494e11db94cb2d343f1 to your computer and use it in GitHub Desktop.
Save PamBWillenz/8494e11db94cb2d343f1 to your computer and use it in GitHub Desktop.
Blocks
INSTRUCTIONS
The map method is one of the most common methods you'll call on an array, along with each.
The map method iterates over an array and passes each element to a block, just like each. Unlike each, the return value of the block is put into a new array which is ultimately returned.
Note: The array returned from map is not the original array, so the original array will not be modified:
array = [1,2,3]
new_array = array.map { |item| item * 5 }
#=> [5, 10, 15]
array
#=> [1, 2, 3]
new_array
#=> [5, 10, 15]
Mutation
What if we want to alter the original array? For many block methods, including map, Ruby provides a "mutative" version, which behaves identically but "mutates" the data the method is called upon. These methods names generally look like their normal counterparts, but are followed by a "bang" (!).
Remember the general purpose of "bang" methods? They are generally "dangerous", because, unlike most methods, they raise errors or modify, rather than return new, data.
Let's compare map and map!:
array = [1,2,3]
array.map { |item| item * 5 }
#=> [5, 10, 15]
array
#=> [1, 2, 3]
array.map! { |item| item * 5 }
#=> [5, 10, 15]
array
#=> [5, 10, 15]
Be careful with bang methods. They can permanently delete or overwrite data!
What do you think select!, reject!, and sort! do? Why do you think each doesn't have a bang method?
Block Shortcuts
Let's use block methods to mutate an array of symbols into an Array of capitalized strings, sorted alphabetically:
original_array = [:kim, :ralph, :bob, :belle]
original_array.map!{ |s| s.to_s }
#=> ['kim', 'ralph', 'bob', 'belle']
original_array.map!{ |s| s.capitalize }
#=> ['Kim', 'Ralph', 'Bob', 'Belle']
original_array.sort!
#=> ['Belle', 'Bob', Kim', 'Ralph']
original_array
#=> ['Belle', 'Bob', Kim', 'Ralph']
Note that our original_array has changed as a result of the bang methods. If we'd called map and sort (without the bangs), this would not have happened.
That worked, but it takes up a lot of space. Chaining the methods will save some (vertical) room, but there's lower hanging fruit here.
We followed a common block pattern in the two map! calls above:
array.block_method{ |element| element.method }
This pattern -- returning the block argument with a single method called on it -- is so common in Ruby that the language has a built-in shortcut. In Ruby, (&:method_name) is equivalent to { |item| item.method_name }.
(&:method_name) basically means: "Return from each block invocation the value of calling method_name on the current element."
Using this shortcut and chaining, our new code is notably brief (and clear):
symbol_array = [:kim, :ralph, :bob, :belle]
symbol_array.map!(&:to_s).map!(&:capitalize).sort!
symbol_array
#=> ["Belle", "Bob", "Kim", "Ralph"]
Create an add_two method that takes an array of numbers and returns an array of strings. For example:
add_two([1,3])
#=> ["1 + 2 = 3", "3 + 2 = 5"]
You can use string interpolation to format the result properly:
def add_two(map_this_array)
# Call map on the map_this_array array.
# The logic of what to do with each element goes inside a block.
# HINT: Remember to interpolate
end
By now you know that specs are very picky. Remember to watch your spaces in the interpolated strings!
SPECS
describe "add_two" do
it "adds 2 to each element in an array" do
a = [1, 2, 3]
r = ["1 + 2 = 3", "2 + 2 = 4", "3 + 2 = 5"]
expect( add_two(a) ).to eq(r)
end
it "adds 2 to each element in a longer array" do
a = [5, 7, 3, 12, 15]
r = ["5 + 2 = 7",
"7 + 2 = 9",
"3 + 2 = 5",
"12 + 2 = 14",
"15 + 2 = 17"]
expect( add_two(a) ).to eq(r)
end
end
CODE
def add_two(map_this_array)
map_this_array.map { |item| "#{item} + 2 = #{item + 2}" }
#Shortuts below
# map_this_array.map(&:+.add_two(2)).map(&:to_s)
# a.map(&:+.with(2))
# v.map(&:to_s) Is the same as: v.map { |i| i.to_s }
end
INSTRUCTIONS
Calling a Method on an Object
In our Advanced Classes checkpoint, we covered the use of self in classes. self, when called within a method, refers to the object upon which the method was called:
class ThunderPerson
attr_accessor :first_name
def initialize(first_name)
@first_name = first_name
end
def full_name
# `self` is optional here. If left out, it is implied.
"#{self.first_name} Thunder"
end
def catchphrase
# `self` is optional here. If left out, it is implied.
"#{self.full_name} lives on water, feeds on lightning!"
end
end
johnny = ThunderPerson.new('Johnny')
johnny.catchphrase
#=> "Johnny Thunder lives on water, feeds on lightning!"
Ruby's built-in collection methods operate this way as well. You call them on a collection, rather than passing the collection in. Then, the operations are run on self (the collection on which the method was called), rather than an argument collection. Because of this, self is the implicit argument of nearly every Ruby method we've used.
If we were to rewrite our new_each method to use self rather than an explicit array argument, it would look something like this:
def new_each
0.upto(self.length - 1) do |index|
yield( self[index] )
end
end
Rather than this:
def new_each(array)
0.upto(array.length - 1) do |index|
yield( array[index] )
end
end
Nice! But how do we actually do this? We define the method on the class on whose instances we'd call it. So a new_each method on the Array class would look like this:
class Array
def new_each
0.upto(self.length - 1) do |index|
yield(self[index])
end
end
end
[1,2,3,4].new_each { |element| p element }
#=> 1
#=> 2
#=> 3
#=> 4
Why a Monkey? Why a Patch?
Ruby actually allows you to do just this. You can "monkey patch" Ruby's core classes to add functionality. When you do this, you're basically throwing some new behavior into the "bag" that is the greater, pre-defined String, Array, or Hash class.
We can, in fact, add this functionality straight into Array. The procedure doesn't differ noticeably from how one might create a new class. Copy the above class code (and method), paste it into IRB, and then try using it. Try using preexisting array functionality. It still works, because we haven't overwritten Array; we've just added to it.
Then close IRB, open it back up, and try using new_each on an array again:
NoMethodError: undefined method `new_each' for [1, 2, 3, 4]:Array
That's because we didn't actually permanently change the Array class, we just "monkey patched" it within the scope of the IRB session we had open.
In Rails, monkey patching is considered a bad practice. This is for several reasons:
It's dangerous. While declaring class Array doesn't overwrite the class, you can specifically overwrite the class's methods, locally. If you got carried away and wrote a to_i method on the String class that took any string and replaced it with the letter "i", you would be (locally) overwriting the far more useful String#to_i method in Ruby core.
It's not modular. If you wrote a format_as_phone_number method on Fixnum that took a number, say 0123456789, and converted it to a phone-formatted string ((012)-345-6789), it'd be useful, but it wouldn't belong there. That's really more the purview of a new PhoneNumber class (which could inherit from string). Coders try to keep classes (and files, and methods) short and single-purpose. The more behavior you add to a core class that's peripheral to its fundamental purpose, the worse.
It doesn't follow the pattern. Ruby and Rails methods and classes tend to follow patterns designed to make them easy to undersand, use, chain, and combine. For instance, most Fixnum methods return a Fixnum; format_as_phone_number returns a String. If we added our format_as_phone_number method to the Fixnum class, we'd be breaking the pattern, which would cause confusion for other coders, including us, later on. (Another pattern-breaking issue with the above method is that it can only be called on numbers 10-digits in length.)
It's low-level. The more basic the code you change, the more it affects. Especially when you're learning, it's a good idea to avoid making sweeping changes to far-reaching code.
Generally, be careful monkey patching core Ruby or Rails classes. Try to avoid it until you're extremely comfortable with the language or framework.
Let's Do Some Monkey Patching!
That said, monkey patching is extremely useful in discussing blocks, and it's an important concept to understand, so let's do a little monkey patching in a safe environment.
Most of the block methods we've been discussing are called directly on method instances, like our rewrite of new_each above. So let's try rewriting some of those methods on the classes on which they're called.
Before you get started on the exercises, let's walk through a more complex example -- implementing a mutative new_map! method on the Array class.
We could start by calling map on self:
class Array
def new_map!
self.map
end
end
But this should look a little strange to you, because we're not passing a block to map. Without a block "argument", map won't know what to do to the array (self).
Blocks are, essentially, a type of argument. We "pass" a block to a method, and the method "invokes" it.
We want to pass the block with which our new_map! method is called to the map called within it. Ruby provides a way to reuse blocks, by converting them into objects that can be passed around like Strings, Integers or Booleans.
Let's take a look at the syntax to reuse a block:
class Array
def new_map!(&block)
self.map(&block)
end
end
Note, the argument does not need to be named "&block". It could be named anything, as long as it has an & to signify that it should be converted to an object.
By placing an & in front of the argument named block, you are telling Ruby to treat it like a reusable object, which can be invoked. This means that new_map! can be called like this:
[1,2,3].new_map! { |num| num * 5 }
This block argument { |num| num * 5 } will be passed to map (non-bang) within the new_map! (bang) method.
Unfortunately, we're still not doing anything different than the original map method. We've simply found another, less direct, way of calling it.
Basically, we need to save the change that is defined in the block argument. Ruby has a helpful method for this, named replace. The replace method replaces the array it's called on, with the array that's passed to it. Consider the following example, using replace:
numbers = [1,2,3]
letters = ["a", "b"]
numbers.replace(letters)
p numbers
#=> ["a", "b"]
Using replace and the & syntax, we can complete our new_map! implementation:
class Array
def new_map!(&block)
self.replace( self.map(&block) )
end
end
Broken down: new_map! expects a block argument. It passes that argument, using the & syntax, to map. map is called on self and runs with the block we passed in. The return value of running map on self with the given block is then used to replace the original array -- self.
Write a new new_map method that is called on an instance of the Array class. It should use the array it's called on as an implicit (self) argument, but otherwise behave identically.
Write a collapse method called on a String instance that returns the string without any spaces. You can use the familiar methods String#split and String#join or you can investigate String#delete.
Write a collapse! method which uses the collapse (no-bang) method to mutate the string on which it's called into a string without whitespace.
Write a new_select! method that behaves like the select, but mutates the array on which it's called. It can use Ruby's built-in collection select method.
You can start with the following scaffolding:
class Array
def new_map
end
def new_select!(&block)
end
end
class String
def collapse
end
def collapse!
end
end
Bonus -- use implicit self, so that you don't actually write self once in your solutions.
SPECS
describe Array do
describe '#new_map' do
it "returns an array with updated values" do
array = [1,2,3,4]
expect( array.new_map(&:to_s) ).to eq( %w{1 2 3 4} )
expect( array.new_map{ |e| e + 2 } ).to eq( [3, 4, 5, 6] )
end
it "does not call #map" do
array = [1,2,3,4]
array.stub(:map) { '' }
expect( array.new_map(&:to_s) ).to eq( %w{1 2 3 4} )
end
it "does not change the original array" do
array = [1,2,3,4]
expect( array.new_map(&:to_s) ).to eq( %w{1 2 3 4} )
expect( array ).to eq([1,2,3,4])
end
end
describe '#new_select!' do
it "selects according to the block instructions" do
expect( [1,2,3,4].new_select!{ |e| e > 2 } ).to eq( [3,4] )
expect( [1,2,3,4].new_select!{ |e| e < 2 } ).to eq( [1] )
end
it "mutates the original collection" do
array = [1,2,3,4]
array.new_select!(&:even?)
expect(array).to eq([2,4])
end
end
end
describe String do
describe "collapse" do
it "gets rid of them white spaces" do
s = "I am a white spacey string"
expect(s.collapse).to eq("Iamawhitespaceystring")
end
it "doesn't mutate" do
s = "I am a white spacey string"
s.collapse
expect(s).to eq("I am a white spacey string")
end
end
describe "collapse!" do
it "mutates the original string" do
s = "I am a white spacey string"
s.collapse!
expect(s).to eq"Iamawhitespaceystring"
end
end
end
CODE
class Array
def new_map
new_array = []
each do |item|
new_array << yield(item)
end
new_array
end
def new_select!(&block)
self.replace(self.select(&block))
end
end
class String
def collapse
split.join
end
def collapse!
replace ( collapse )
end
end
RESULTS
Array#new_map returns an array with updated values
Array#new_map does not call #map
Array#new_map does not change the original array
Array#new_select! selects according to the block instructions
Array#new_select! mutates the original collection
String collapse gets rid of them white spaces
String collapse doesn't mutate
String collapse! mutates the original string
INSTRUCTIONS
Where does that yield method come from? It's just Ruby for "call the block-defined method."
Calling a block with yield is referred to as "invoking" the block.
When you include yield in a method, it expects a block that it can invoke:
def return_bigger(array)
array.map do |item|
yield(item)
end
end
return_bigger(array)
#=> LocalJumpError: no block given (yield)
Let's implement a new_each method that takes an array as an argument and does with each element whatever we specify in the block:
new_each([1,2,3,4]) do |item|
p "Whatever I want! Item: #{item}"
end
Why do we have to pass in the array as an argument, rather than call the method on the array, like we can with each? We'll discuss this in the next exercise, on "Monkey Patching."
To make this work, we'll have to use yield. Our new_each method will take its array argument, and iterate through each element, invoking the block instructions for each element, using yield. In pseudo-code:
def new_each(array)
# loop through each element
# invoke the block code with the element as a block argument
# close the loop
end
To loop through the array without using each, we can use array indexing and the length of the array. Then to run the block code, we use yield with our current element as an argument:
def new_each(array)
0.upto(array.length - 1) do |index|
yield( array[index] )
end
end
Let's make a similar new_each_with_index method to test out multiple yield arguments:
def new_each_with_index(array)
0.upto(array.length - 1) do |index|
yield(array[index], index)
end
end
num_array = %w{one two three four}
new_each_with_index(num_array) do |e, i|
p "The element at location #{i} is '#{e}'"
end
#=> "The element at location 0 is 'one'"
#=> "The element at location 1 is 'two'"
#=> "The element at location 2 is 'three'"
#=> "The element at location 3 is 'four'"
Let's break this down:
We call new_each_with_index with an array argument.
We cycle through each index in the array, and calculate the element at that location by indexing into the array -- (array[index]).
We invoke whatever block is given using the yield keyword, passing in the element and index as "block arguments".
When yield invokes the block, it runs that anonymous function right in the new_each_with_index method, so that our code essentially becomes:
def new_each_with_index(array)
0.upto(array.length - 1) do |index|
p "The element at location #{index} is '#{array[index]}'"
end
end
Define a new_map function. It should take an array as an argument and return a new array modified according to the instructions passed in as a block. Feel free to use each within the method, rather than the array indexing we used above.
The first step in re-implementing map should be to iterate over the array:
def new_map(array)
array.each do |item|
end
end
The new_map method will be quite similar to our new_each method, but rather than just performing "side effect" behavior with each element, you'll want to store the return value from each block invocation in a new array:
def new_map(array)
new_array = []
array.each do |item|
# invoke the block, and add its return value to the new array
end
end
When you've finished iterating through the old array, just return the new one from your new_map function.
SPECS
describe "new_map" do
it "should not call map or map!" do
a = [1, 2, 3]
a.stub(:map) { '' }
a.stub(:map!) { '' }
expect( new_map(a) { |i| i + 1 } ).to eq([2, 3, 4])
end
it "should map any object" do
a = [1, "two", :three]
expect( new_map(a) { |i| i.class } ).to eq([Fixnum, String, Symbol])
end
end
CODE
def new_map(array)
new_array = []
array.each do |item|
new_array << yield(item)
end
new_array
end
INSTRUCTIONS
In this exercise you'll create two methods. The first method should be named sort_by_length. This method should take an array of strings or hashes as an argument, and sort each element by its length. Let's start by defining the method:
def sort_by_length(sort_this_array)
end
Ruby's Array class has a method that will help us implement a solution. The sort method in Ruby's Array class takes a block as an argument and sorts the elements of the array it's called on, according to the logic in the block. Consider the following example using the sort method:
letters = [ "d", "a", "e", "c", "b" ]
letters.sort { |x,y| y <=> x }
#=> ["e", "d", "c", "b", "a"]
Remember that { |block_arg| code here } is equivalent to the longer do end block syntax.
In the block passed to sort, we're using a "spaceship operator" (<=>) to determine the order of the elements. The spaceship operator returns 1,0 or -1 based on the value of the left argument relative to the value of the right argument, and the sort method the orders items from negative to positive. For example: a <=> b returns:
-1 if a < b
0 if a == b
1 if a > b
If we switch x and y in the sorting block, we reverse the ordering rule, and the items are sorted in the opposite direction:
letters = [ "d", "a", "e", "c", "b" ]
letters.sort { |x,y| x <=> y }
#=> ["a", "b", "c", "d", "e"]
The mechanics of the spaceship operator can be confusing and aren't necessary to nail down at first, so long as you can use it to control ordering directionality.
If no block argument is passed to the sort method, a spaceship operator will be used by default, and Ruby will make assumptions about what determines correct ordering:
letters.sort
#=> ["a", "b", "c", "d", "e"]
numbers = [2,3,1]
numbers.sort
#=> [1,2,3]
We can use the sort method, along with a spaceship operator to sort a given array by the length of its elements:
def sort_by_length(array)
array.sort { } # sorting logic goes inside the block argument
end
Hint: Remember that the array argument above (array) is an array made up of strings or hashes. Both strings and hashes have a length method.
Create another method named filter that takes an array of numbers as an argument and returns an array consisting of numbers that are greater than 5. Let's define the method:
def filter(array)
end
This is a great case for Ruby's select method. Consider the following refresher example using select:
[1,2,3,4,5].select do |num|
num.even?
end
#=> [2, 4]
In the example above, we passed a block to select that picked the even numbers in the given array ([1,2,3,4,5]), and returned the result as an array ([2,4]).
We can use the select method to return an array consisting of numbers greater than 5:
def filter(array)
array.select { } # filter logic goes inside the block argument
end
SPECS
describe "sort_by_length" do
it "sorts an array of strings by length" do
a = %w(z yyyy xxx ww)
sorted = %w(z ww xxx yyyy)
expect( sort_by_length(a) ).to eq(sorted)
end
it "sorts hashes by length" do
a = [{a: "a", b: "b"}, { key: "value"}, {}]
sorted = [{}, { key: "value"}, {a: "a", b: "b"}]
expect( sort_by_length(a) ).to eq(sorted)
end
end
describe "filter" do
it "returns numbers greater than 5 in a small array" do
expect( filter([1, 3, 7, 8]) ).to eq([7, 8])
end
it "returns numbers greater than 5 in a large array" do
a = [1, 2, 17, 56, 7, 12, 3, 18, 19, 23]
r = [17, 56, 7, 12, 18, 19, 23]
expect( filter(a) ).to eq(r)
end
end
CODE
def sort_by_length(sort_this_array)
sort_this_array.sort { |x,y| x.length <=> y.length }
end
def filter(array)
array.select { |num| num > 5 }
end
RESULTS
sort_by_length sorts an array of strings by length
sort_by_length sorts hashes by length
filter returns numbers greater than 5 in a small array
filter returns numbers greater than 5 in a large array
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment