Here's Friday 2/28/2020:
Continuing on with the next section:
Introduction to Enumerables
key terms
mapping: to "poll" every member in a collection and collect the results into a new collection
reducing: to "poll" every member in a collection and join those poll's results together into a single value through a process called "reducing"
"wrapping' - I don't think I defined this for myself. The content seems to assume you can figure out what they mean by wrapping -- as a kind of pedogical word play to help you visualize what's happening. By wrapping, they mean to put something within brackets/parentheses/curly braces to create either a method or collection object (like an array, hash, etc)
Enumerable
enumerable is a ruby-specific term for any built-in method that:
1. iterates through each element in a collection (array, hash, etc)
2. tests each element or uses each element to return a result
3. returns a new collection of those returned results or accumulates those returned results into a single value
The word enumerable (for these methods like .each etc) comes from the verb, enumerate, which means to mention a number of things, one by one. The latin root enumerat, meaning 'count out' (e for ex and numerat from numerus (meaning number).
There's a general template for all enumerable methods:
1. give a collection
2. based on the number of items, visit each one by its location int he series (this is the ex numerus part - count out)
3. Do some work or test the elements or pairs in the collection (why does this definition mention 'pairs' all the time? Doesn't have to, we know to apply it to arrays of arrays, etc.
4. Depending on the job:
1. accumulate those elements after whatever we did to them into a new collection
2. or determine a special value (maximum/minimum, condition: boolean conclusion: false if no truthy values, true if all true or perhaps true if no values were truthy, etc)
The text refers to this template as the "character" of all enumerable methods. like a personality?
Pseudocode - already think in these terms thanks to college CS classes
Next:
Introduction to Map and Reduce Lab
this lab is going to have me build my own versions of map and reduce methods
remember that DRY means "Don't Repeat Yourself"
Map:
comes from mathematics where it means to take a variable, plug it into an equation and get a result
Reduce:
basically the idea that you cook something down (apply work) and use what's left over
all map methods return a new array
all reduce methods return a value
Create the following methods:
map_to_negativize(source_array)
returns an array with all values made negative
map_to_no_change(source_array)
returns an array with the original values
? what does this mean exactly? just return source_array?
map_to_double(source_array)
returns an array with the original values multiplied by 2
map_to_square(source_array)
returns an array with the original values squared
reduce_to_total(source_array, starting_point)
returns a running total when given a starting point
starting point is a number to add the values of the array to
returns a running total when not given a starting point
need to use a default in the parameter:
reduce_to_total(source_array, starting_point = 0)
worked!
reduce_to_all_true(source_array)
returns true when all values are truthy
returns false when any value is false -- check for this at the top of the if statement
worked!
reduce_to_any_true(source_array)
returns true when a truthy value is present
returns false when no truthy value is present
worked!
Next!
Generalized Map and Reduce Lab
* avoid duplication by using blocks
* are these different from code blocks I'm thinking of?
* ah, this is a Ruby specific term different form the general concept of a block of code
* two ways of construct blocks
* wrap the code in curly braces! {}
* used only when the code is one expression
* don't need return keyword
* use the keywords: do ... end
* used when the code takes multiple lines
* last line of do...end block will be the return value
* start by using do...end blocks
* makes it easy to add debugging lines for printing out values later
* once everything's correct, ok to switch to curly braces {}
* execute a block from within a method
* use the yield keyword
* purpose of the yield keyword
* yield executes the block passed into the method
* it's basically the 'return' keyword except for these blocks
* Use it inside of a method you plan to use with blocks?
* oh, a block is meant to be used with a method call -- VERY IMPORTANT TO KEEP IN MIND!!!!
* IT'S ONLY EVER USED WITH CALLS TO METHODS, NOT THE INSIDE CODE OF THE METHOD
* that's why it's like you're passing the arguments from inside the method to the block
* the method is the general instructions for what you want to happen
* you plan for it to take blocks as part of its usage and use yield for the express purpose of using blocks
* it yields the result of the code inside of the method to the block
* ohh, so not like return -- you use yield to pass code to the block...right?
* so yield calls the block
* so yield can be or actually is basically a placeholder for the block code
* when the code reaches 'yield', the code inside the block gets executed. when the code inside the block finishes, the execution of the method continues.
* the block is used to customize the results and basically do anything you want without declaring another method
* suddenly the funny term hashketball is starting to make more sense
* it's lesson 95 and I'm almost there. 9 more to go!
* parameters
* parameters for blocks are called block-parameters
* use pipe character instead of parentheses: | |
* multiple block parameters: separate with a comma like regular parameters
* pass data between methods and blocks
* then, when you call a method, immediately proceed with do, then the block parameter(s) between pipes, the code, and then close it with end
* it's cool because no matter how many arguments the method takes, you just need one block-parameter to handle it.
example:
1. def make_sandwich(element1, element2)
2. base = "A #{element1} and #{element2}"
3. yield(base)
4. end
5.
6. make_sandwich("gator", "gumbo") do |innards|
7. "#{innards} on rye"
8. end #=> "A gator and gumbo on rye"
this can be written using curly braces {} since it's one expression and very simple:
make_sandwich("gator", "gumbo") { |innards| "#{innards} on rye" }
#=> "A gator and gumbo on rye"
Lab:
write a generalized map and reduce method
* both take a a bock
* both need data passed between the method and the block
map - all the methods we did last lab, but now with just one method and blocks -- as per the tester
map(array) {|a| a*a}
1. Arguments
1. source array
2. yields(each item in source array)
3. returns
1. array
Ok, this is annoying. They showed bascially what yield is but didn't show us exactly how its used. But they did say the goal is to be able to look up in the documentation. Jesus.
Ok, so yield takes parameters yo.
it should be taught this way:
yield is a stand-in for block and will pass values to block.
the value passed is basically an argument -- so yield(value) is what you'll most often be using.
Then you can use block to basically add your own expressions
reduce
1. arguments
1. source array
2. starting value default to 0
2. yield(each item in source array)
3. returns a value
Ah hah, I should have had an if statement checking if a starting_point was provided. Tyler on learnco guided me:
def reduce(source_array, starting_point = nil)
if starting_point
new = starting_point
i = 0
else
new = source_array[0]
i = 1
end
while i < source_array.length do
new = yield(new, source_array[i])
i += 1
end
return new
end
Big help!
Next!
Enumerable Family Tree
* map and reduce are built-in methods in the Array Class
* use map to transform an array
* [10, 20, 30, 40].map{ |num| num * 2 }
* use reduce to reduce an array to a value
* [10, 20, 30, 40].reduce(0){ |total, num| total + num}
* [10, 20, 30, 40].reduce(100){ |total, num| total + num }
* use Ruby documentation to learn more about other variations on map an reduce
* Now I can understand how to use most of the methods in the documentation now!
* At first, the blocks were confusing
* but now that I know what they are and what they do, it really opens the methods up to what I could do with them
* use select and reject to filter an array
* .select returns an array containing all elements for which the given block returns a true value
* .filter does the same thing
* [10, 20, 30, 40].select{ |num| num > 25 }
* .reject returns an array for all elements for which the given block returns false
* [10, 20, 30, 40].reject{ |num| num > 25 }
* memorize a list of enumerables
* all?
* Everything "tested" by the block returns truthy
* any?
* Did anything "tested" by the block returns truthy
* collect
* A synonym for map
* count
* Which elements satisfy the block or, without block, how many elements are there?
* detect
* Which element satisfies the block first. Does the same thing as find.
* find_all
* Which elements satisfy the block?
* find_index
* What is the index of the first element to satisfy the block?
* max
* What's the highest value?
* max_by
* What's the highest value based on some property of the element?
* min
* What's the lowest value?
* sort
* Put the values in order
Next!
Each: The unexpressive Enumerable
* .each is the root of all the mapping/reducing-like methods
* use each to work with an array
* explain why each is the least-expressive Enumerable
* it should be use the least out of all the methods
* because it's the least-expressive
* .each is generic
* you can reinvent all those other methods using .each
* shouldn't do this because it skips the documentation part of programming and leads to poor readability
* it's not as easy to see what a method that has .each is doing as one that using a more specific method
* code should communicate first and work second
* when to use .each
* enumerate a collection but not transforming the data
* when you aren't sure which Enumerable you should use
* best use is to print to the screen
* though you can always just use p with the result of a more specific Enumerable
* variants of .each
* each_cons
* each_entry
* each_slice
* each_with_index
* each_with_object (a cousin of reduce)
* .each is the most flexible, but also the least expressive
* most of the time you'll be using map and reduce
* use when you aren't sure which other methods you should use
Next!
Hashes & Enumerables
Ah, the real goal -- using enumerables with hashes. the Hash class has all the same enumerables as Array except some are less useful than they are for Arrays
* use .each and .each_pair to print out a hash
* use reduce to create a transformed hash
* use reduce to resolve a value from a hash
* many programmers forget to return the 'memo' at the end and have a tricky bug because of it
* use the hash, the each, and the hash-relevant each_pair to help
* print out a hash using .each and .each_pair (see below for example hash)
**the first block parameter is conventionally called 'memo' -- saw this in the previous lab's tester, but didn't know why it was chosen for the block parameter
example:
bands = {
joy_division: %w[ian bernard peter stephen],
the_smiths: %w[johnny andy morrissey mike],
the_cramps: %w[lux ivy nick],
blondie: %w[debbie chris clem jimmy nigel],
talking_heads: %w[david tina chris jerry]
}
bands.each{ |pair| p pair}
#=>
# [:joy_division, ["ian", "bernard", "peter", "stephen"]]
# [:the_smiths, ["johnny", "andy", "morrissey", "mike"]]
# [:the_cramps, ["lux", "ivy", "nick"]]
# [:blondie, ["debbie", "chris", "clem", "jimmy", "nigel"]]
# [:talking_heads, ["david", "tina", "chris", "jerry"]]
.each_pair is more expressive:
it does the same thing in this case
if you want to change anything, reduce is better:
bands.reduce({}) do |memo, pair|
p memo #First block parameter
p pair #Second block parameter
memo #Return value for the block. It becomes the memo in the next go-around
end
{}
[:joy_division, ["ian", "bernard", "peter", "stephen"]]
{}
[:the_smiths, ["johnny", "andy", "morrissey", "mike"]]
{}
[:the_cramps, ["lux", "ivy", "nick"]]
{}
[:blondie, ["debbie", "chris", "clem", "jimmy", "nigel"]]
{}
[:talking_heads, ["david", "tina", "chris", "jerry"]]
our 'accumulating' hash called memo (the first block parameter) is the thing we need to update. but keys and values are returned as 2-element arrays in each call to the block.
how do we split each array into key and value?
we could use pair[0] and pair[1]
Ruby has a nicer way to do this kind of work called: "destructing assignment." This means using parentheses for that block parameter, and listing key and value, separated by a comma.
bands.reduce({}) do |memo, (key, value)|
p memo #first block parameter
p key #Second block parameter
p value #Second block parameter
memo #Return value for the block, becomes the memo in the next iteration
end
1. #=>
2. # {}
3. # :joy_division
4. # ["ian", "bernard", "peter", "stephen"] ... etc.
"Thanks to destructuring assignment (using the parentheses), we crack open the Array that was in the pair parameter and put element 0 in key and element 1 in value. With this in place, it's easy to create that alphabetized roster."
sorted_member_list = bands.reduce({}) do |memo, (key, value)|
memo[key] = value.sort
memo
end
printing them out:
p bands
p sorted_member_list
{:joy_division=>["bernard", "ian", "peter", "stephen"], :the_smiths=>["andy",
"johnny", "mike", "morrissey"], :the_cramps=>["ivy", "lux", "nick"],
:blondie=>["chris", "clem", "debbie", "jimmy", "nigel"],
:talking_heads=>["chris", "david", "jerry", "tina"]}
Another example:
With Hashes, we also use reduce to accumulate to a single value. Let's find first-most alphabetical band member of the entire Hash
bands = {
joy_division: %w[ian bernard peter stephen],
the_smiths: %w[johnny andy morrissey mike],
the_cramps: %w[lux ivy nick],
blondie: %w[debbie chris clem jimmy nigel],
talking_heads: %w[david tina chris jerry]
}
firstmost_name = bands.reduce(nil) do |memo, (key, value)|
# On the first pass, we don't have a name, so just grab the first one.
memo = value[0] if !memo
# Sort that array of names
sorted_names = value.sort
# If string comparison says the sorted name of the array is earlier than
# the memo, it becomes the new memo.
memo = sorted_names[0] if sorted_names[0] <= memo
# Return the memo as per reduce rules
# (Try adding 'p' in front of memo to see how it changes as the enumerate the
# pair of the hash!)
memo
end
p firstmost_name
"andy"
Master .reduce to transform a given hash into a new hash is a sign of a comfortable ruby programmer.
time for a quiz!
Ruby Enumerables Quiz
quizes have challenges now!
1st challenge:
use .map
lunch_menu.map {|item| "#{item}!" }
#adds an exclamation point to each item in the array lunch_menu
2nd challenge:
use .collect ....?
remember that .collect is a synonym for .map
variable, nums
set equal to an array of numbers
nums = [1,2,3,4]
enumerate of nums and return a new array of squares
nums.map{|n| n*n}
remember it's the same as .map, so:
nums.collect{ |n| n*n }
3rd challenge:
use .select
select: returns a new array containing all elements of the array for which the block returns a true value
nums = [2,4,3,5,7,6,9]
find all even numbers:
nums.select{|n| n % 2 == 0 }
4th challenge
use .find to return the first array element that's an odd number
nums = [2,4,3,5,7,6,9]
nums.find { |n| n % 2 == 1 } #this would work
nums.find{ |n| n.odd? } #this would also work
5th Challenge
use .include?
use .include? to check and see if an array includes a specific string
array_of_strings.include?("Maru) #.include? returns index if you provide a block instead of an argument
and done!
On to the labs now:
Reverse Each Word Lab
* understand the return value of the .each method
* use the .collect method
* understand the .collect method
* understand the return value of the .collect method
* use the return of collect for further operation
Instructions:
* write a method called reverse_each_word that takes in a string argument of a sentence and returns that same sentence with each word reversed in place.
* first make the method using .each
* then make the method using .collect
* notice the difference
example:
reverse_each_word("Hello there, and how are you?")
#=> "olleH ,ereht dna woh era ?uoy"
you can't use enumerators on a string, you must turn the string into an array first. How do you do that?
how do you reverse each word and return them? .each returns the original array but other enumerators don't
Ah hah, the tester doesn't test for .each, so screw that. full speed ahead with .map!
Ok, looks like I can use .collect in the whole method and that'll pass the lab.
And that's it:
def reverse_each_word(string)
string_array = string.split
reversed_words = string_array.collect {|word| word.reverse}
reversed_string = reversed_words.join(" ")
return reversed_string
end
Next lab:
Cartoon Collections Lab
Here we go!
* practice more array iteration with with enumerator methods like .collect or .map, .find, and .include?
* build methods and control their return values
* practice if/else statements for control flow
Instructions:
4 Methods:
* roll_call_dwarves
* Argument: array of dwarf names
* print out each name in number order using puts
* wtf is number order? the index order?
* looking at the tester, it explains much better:
* it means you create a numbered list, doesn't matter what the order of the dwarf names is -- just make sure the names are listed with numbers: 1. Dopey, 2. Grumpy, 3. Bashful, etc.
* I'll try it with new lines, all as text
* look into the each_with_index method
* use each_with_index block to add each element's index + 1 to the string with interpolation
* then use .join(", ") -- delimiter is comma and space?
* not necessary with each_with_index
* each_with_index worked very well
* summon_captain_planet
* argument: array of planeteer calls
* planeteer_calls = ["earth", "wind", "fire", "water", "heart"]
* Capitalize the first letter of each element
* Add an exclamation point to each
* returns:
* ["Earth!", "Wind!", "Fire!", "Water!", "Heart!"]
* .map or .collect (they're the same)
* long_planeteer_calls
* arguments: array of planeteer calls again
* return true if any of the calls are longer than four characters
* .map it, comparing each element:
* if element.length > 4 return true else return false
* as soon as it hits an element with more than 4 characters, it's going to return true, which is what we want. "tell us if any of the calls are longer than 4"
* no need for else statement -- since return true would break and end the method, if there are no true's, it'll finish iterating and go on to the return false line.
* looks like I don't need .map -- .any? seems perfect
* elements.any? { |call| call.length > 4 }
* worked great!
* find_the_cheese
* arguments: array of strings
* returns:
* first string that is a type of cheese
* nil if there's no cheese in the array
* nil is falsy right? if .include? returns nil, that would be the same as returning false? or no?
* look at .include? method
* .map of cheese array
* block: |cheese| passed_array.include?(cheese)
* no .map, just a while loop and if statement
and this is how you achieve 2 objectives for a string array with one .map call:
planeteer_calls.map do |e|
e.capitalize + "!"
end
def find_the_cheese(say_cheese)
cheese_types = ["cheddar", "gouda", "camembert"]
index = 0
while index < cheese_types.length do
if say_cheese.include?(cheese_types[index])
return cheese_types[index]
end
index += 1
end
end |