Last active
August 29, 2015 14:19
-
-
Save PamBWillenz/8494e11db94cb2d343f1 to your computer and use it in GitHub Desktop.
Blocks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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