Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Magic of Enumerables

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for Annette Annette
January 31, 2018

Magic of Enumerables

A walk through Ruby Enumerable Methods

Avatar for Annette

Annette

January 31, 2018

Other Decks in Programming

Transcript

  1. Enumerable.map do |#method| talk_at_ROR end Hi Today I am talking

    about the magic of enumerables. I am going to start with the more common enumerable methods and finish with some interesting and lesser known enumerable methods.
  2. What is an enumerator? An enumerator is a method to

    traverse, search, sort and manipulate collections.
  3. But what does this mean? An enumerator uses iteration as

    a way to operate on a collection object, such as an array, and do something with each element in the collection.
  4. result = problems.select do |problem| problem == solution end When

    you are using an enumerator you are looking for the enumerable method to do one of a number of things: return something for your program to use.
  5. blocks.each do |block| if block == dinosaur_part dinosaur << block

    end end >> Or create a new object, containing elements from the collection, that satisfy the condition in the block.
  6. stickers.each do |sticker| puts sticker end Or trigger another event

    such as printing out each element of the collection
  7. return values When you are looking for the best enumerable

    method to use, you need to consider the return value that the enumerator will give you. This can sometimes be surprising.
  8. The most frequently used enumerators can be grouped according to

    their return values: the original collection, the altered collection, true or false, a subset of the collection or the result of a search.
  9. letters.each { |letter| letter.upcase } returns r u b y

    r u b y The each method returns the original collection. So the return value is not the result of the block condition iterations but the original collection.
  10. letters.each { |letter| letter.upcase } letters.each do |letter| letter.upcase end

    letters.each(&:upcase) There are three ways to write an each method: • The one liner, great for a short block condition • The block layout, used when the block condition is longer or more than one line • The Ruby ampersand symbol method
  11. (&:method_name) ??? So how does the ampersand symbol method work:

    Whenever Ruby sees the ampersand (&) as part of a parameter, it wants the parameter to be a Proc. If it isn’t a proc, Ruby calls the instance method #to_proc on the object to convert it.
  12. returns r u b y letters.map { |letter| letter.upcase }

    R U B Y The map method returns the collection created using the condition in the block. So where you want to use the new values from the block you would use map rather than each. Map is also known as collect but the ruby style guide prefers map
  13. letters.map { |letter| letter.upcase } letters.map do |letter| letter.upcase end

    letters.map(&:upcase) As with the each method, there are three ways to write a map method: • The one liner • The block layout • The Ruby ampersand symbol method
  14. when we run: letters.map(&:upcase) this is what is happening: :upcase.to_proc.call(“letter”)

    A little more on our Ruby ampersand symbol method In our example, when we call to_proc on the :upcase symbol, it will return a Proc object that calls the upcase method for each letter element in the letters array as it iterates.
  15. all? any? none? include? A boolean enumerator returns true or

    false based on the collection elements meeting the condition in the block
  16. flock.all? { |sheep| “black_faced” } returns true The all? method

    requires every iteration through the block, to be true for the condition in the block, for it to return true.
  17. flock.none? { |sheep| “black_sheep” } returns true The none? method

    requires every iteration through the block, to be false for the condition in the block, for it to return true.
  18. flock.any? { |sheep| “black_sheep” } returns true The any? method

    only requires one iteration through the block, to be true for the condition in the block, to return true
  19. flock.include? “donkey” returns true The include? method only requires one

    element in the collection, to match the condition in the block, to return true
  20. U B R Y select detect reject A search enumerable

    method returns the result of the condition given to the block
  21. cats.select { |cat| “black_cat” } returns The select method returns

    a new collection of elements that meet the condition in the block.
  22. cats.detect { |cat| “grey_cat” } returns The detect method returns

    only the first element that matches the condition in the block
  23. cats.reject { |cat| “black_cat” } returns The reject method returns

    a new collection of elements that do not meet the condition in the block
  24. r u b y b r u y [“r”, “u”,

    “b”, “y”].sort OR returns [“r”, “u”, “b”, “y”].sort { |a,b| a <=> b } The sort method uses the element’s own combined comparison operator or uses an optional block, to rearrange the elements of the collection. It can handle strings, numbers and classes. The sort method returns a new array without changing the original array. We can use ascending or descending sort orders.
  25. r u b y b r u y [“r”, “u”,

    “b”, “y”].sort! returns and is now You can use sort with a bang(sort!) to permanently change the original array, this reorders the array in-place.
  26. r u b y b r u y returns and

    is now [“r”, “u”, “b”, “y”].sort { |a,b| b <=> a } To sort in descending order you need to use the block format and compare the elements in the opposite order. In the example we are comparing b to a rather than the ascending format of comparing a to b.
  27. ["round", "oval", "square", “triangle”].sort_by do |word| word.size end returns The

    sort_by method uses a set of keys generated by mapping the values in the collection through the given block. The example shown is sorting on the length of each word.
  28. numbers = [1, 2, 4, 6, 8, 2] numbers.count(2) returns

    2 We all know count as a method we can use on arrays and strings to find out the number of elements or characters in them. But the count enumerable method can do more: We can ask how many numbers in the collection match the parameter in the brackets. In the example we are looking to find matches for the 2 parameter which appears twice so the method returns 2.
  29. numbers.count { |num| num > 2 } returns 3 numbers

    = [1, 2, 4, 6, 8, 2] We can ask how many numbers match a condition in the block. The example asks how many numbers are greater than 2 which returns 3 as 4, 6 and 8 match the condition.
  30. a = [4, 5, 6] b = [7, 8, 9]

    a.zip(b) returns [[4, 7], [5, 8], [6, 9]] The zip method takes one element from the collection and merges corresponding elements from each argument. As in element 1 with element 1, element 2 with element 2, and so on.
  31. a = [4, 5, 6] b = [7, 8, 9]

    [1, 2, 3].zip(a, b) returns [[1, 4, 7], [2, 5, 8], [3, 6, 9]] We can also use zip with three arrays, here we iterate through the first array zipping in arrays a & b as we go to return the new array
  32. a = [4, 5, 6] b = [7, 8, 9]

    c = [] a.zip(b) { |x, y| c << x + y } returns nil c returns [11, 13, 15] The zip method also allows a condition in the block to operate on the zip result. The method returns nil but the c array holds the result of the block condition.
  33. bikes = [“mountain”, “racing”, “bmx”] bikes.cycle(2) { |bike| puts bike

    } returns nil puts result: mountain, racing, bmx, mountain, racing, bmx The cycle method calls the block for each element of the collection. It repeats the cycle n times or forever if no argument is given. The cycle method returns nil. The example gives the method a parameter of two so it cycles through the collection twice, returning nil, and putting out the block condition twice for each element.
  34. [1, 2, 3, 4].flat_map { |num| [num, -num] } returns

    [1, -1, 2, -2, 3, -3, 4, -4] [1, 2, 3, 4].map { |num| [num, -num] }.flatten returns [1, -1, 2, -2, 3, -3, 4, -4] The flat_map method returns a new array with the concatenated results of running each element through the block once. It is in effect the equivalent of using map with the flatten method as shown in the second example.
  35. So why flat_map? tobi@happy ~/github/ruby_playground $ ruby benchmark/flat_map.rb Warming up

    -------------------------------------- flat_map 95.000 i/100ms map.flatten 42.000 i/100ms Calculating ------------------------------------- flat_map 972.133 (± 5.1%) i/s - 4.845k in 5.002625s map.flatten 423.280 (± 2.4%) i/s - 2.142k in 5.063801s Comparison: flat_map: 972.1 i/s map.flatten: 423.3 i/s - 2.30x slower map.flatten: 423.3 i/s - 2.30x slower So why use flat_map? It is over two times faster than using map with flatten!
  36. bm.report "flat_map" do list.flat_map do |x| [x, x * x]

    end end bm.report "map.flatten" do list.map do |x| [x, x * x] end.flatten end And this benchmark test used a simple method! Imagine the difference when used with deeply nested iterations!
  37. (1..6).group_by { |num| num % 3 } returns { 0

    => [3, 6], 1 => [1, 4], 2 => [2, 5] } The group_by method groups the collection by the result of the block. It returns a hash where the keys are the evaluated result from the block and the values are arrays of the elements in the collection that correspond to the key. In the example we can see the first key is a zero remainder and the values that get a zero remainder from the block are 3 and 6.
  38. balls.partition { |ball| ball == “blue” } returns The partition

    method returns two arrays, the first containing the elements for which the block was true and the second containing the rest. It is a convenient method to divide an array based on a condition.
  39. (1..5).reduce { |sum, n| sum + n } returns 15

    The reduce method combines all elements of the collection by applying a binary operation. The binary operation can be specified by a block or a symbol, In the example we are passing an accumulator (sum) to a block along with an element of the collection. They are both applied to the block condition on each iteration. It is the value of the accumulator at the end of the iteration that is returned.
  40. (1..5).reduce(:+) returns 15 The binary condition can also be specified

    by a symbol that names a method or operator. In the example the accumulator still exists but is not visible within the method. We are passing the addition operator to reduce on each iteration and it adds each element to the accumulator as it iterates. The final accumulator value is the value returned.
  41. (1..5).reduce(1, :+) (1..5).reduce(1) { |sum, n| sum + n }

    both return 16 With both the block method and the symbol method of reduce you can have an initial starting value for the accumulator. With the block method it is expressed with the value in brackets with the symbol method. With the symbol method it is expressed in brackets after reduce. The returned value is 16, one more than the examples without an initial value. Without an argument for the initial value the first value of the collection is the initial value for the accumulator.
  42. lazy The lazy method returns a lazy enumerator object which

    has the methods map, flat_map, select, reject, and more. The lazy methods enumerate values only on an as- needed basis.
  43. range = 1..Float::INFINITY returns 1..Infinity range.lazy.map { | x |

    x * x }.first(10) returns [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] The lazy enumerator allows you to iterate over an infinite series of values and take only the values you want. In the example we are able to ask for just the first 10 rows. First we set a range from 1 to infinity Then we lazy map over the range applying each number to the block but limit the returned rows to the first 10.
  44. range.lazy.map { | x | x * x } returns

    #<Enumerator::Lazy: #<Enumerator::Lazy: 1..Infinity>:map> Without the 10 row limitation the method returns a lazy enumerator object rather than falling into an infinite iteration.
  45. Huh, how can that work? The key to the way

    lazy map works is that lazy is not eager. The lazy map method does not collect the values into an array first, instead, each value is passed one at a time along the chain of enumerator lazy objects via repeated yields, and therefore can be called upon as required.
  46. range.map { | x | x * x } returns

    infinite iteration i range.map { | x | x * x }.first(10) returns infinite iteration Normal enumerable methods have problems with large collections. The reason is that method is eager rather than lazy and wants to iterate over the whole collection before returning a result. Without the lazy enumerator the non-limited method ends up in an infinite iteration, Even the method limited to returning only 10 rows ends up in an infinite iteration. This is because they won’t return a result until they have iterated through the entire collection, which is infinite!
  47. References: • http://patshaughnessy.net/2013/4/3/ ruby-2-0-works-hard-so-you-can-be-lazy - Ruby 2.0 Works Hard so

    You Can Be Lazy - Pat Shaughnessy - accessed 18/10/2017; • http://ruby.bastardsbook.com/chapters/enumerables/ - The Bastards Book of Ruby - accessed 18/10/17; • https://ruby-doc.org/core-2.5.0/Enumerable.html - Enumerable - accessed 27/01/2018; • http://www.brianstorti.com/ understanding-ruby-idiom-map-with-symbol/ - Understanding Ruby’s idiom: array,map(&:method) - Brian Storti - accessed 27/01/2018; • https://blog.honeybadger.io/ using-lazy-enumerators-to-work-with-large-files-in-ruby/ - Using lazy enumerators to work with large files in Ruby - Starr Horne - accessed 27/01/2018; • https://www.dotnetperls.com/sort-ruby - accessed 28/01/2018. I have included a list of references Thank you for listening to my talk, I hope you were able to get a few things of interest from it. Are there any questions?