Lecture: Higher-order Functions
Dear students:
There are lots of fascinating choices that language designers have made about functions, but we're going to focus on just one very important feature: higher-order functions. Sometimes our algorithms need us to send them not just data, but also chunks of code.
Blocks
Some languages let us put functions in a variable and pass them around as parameters. You can do this in some form in JavaScript, Java, and even C. Ruby does not let us put functions in a variable exactly. But we can pass around code using blocks.
Suppose we had a list of numbers and wanted to measure the jumps between these numbers: this one is up 9 from the previous, this one is down 7. Suppose also we had an abstraction named each_pair that visited each each consecutive pair of elements in an array. We could use it to solve our problem by passing it a block that examined a pair and printed out a description of the delta:
xs = [1, 5, 3, 8, 20]
each_pair(xs) do |a, b|
if b > a
puts "up #{b - a}"
elsif a > b
puts "down #{a - b}"
else
puts "hold"
end
end
xs = [1, 5, 3, 8, 20]
each_pair(xs) do |a, b|
if b > a
puts "up #{b - a}"
elsif a > b
puts "down #{a - b}"
else
puts "hold"
end
endThere's no loop in this code. Nowhere am I getting caught up in the details of the iteration mechanics. The block deals only with a single pair of elements at a time.
The higher-order function each_pair is what actually holds the loop. It walks through the indices and invokes the block using the yield command:
def each_pair(items)
i = 0
while i < items.size - 1
yield(items[i], items[i + 1])
i += 1
end
end
def each_pair(items)
i = 0
while i < items.size - 1
yield(items[i], items[i + 1])
i += 1
end
endAny time we need to process consecutive pairs in an array, we can call upon the very reusable each_pair. However, we'd be better off using Ruby's builtin each_cons.
Sometimes we have two separate or parallel arrays that we want to process in tandem, like names and IDs, book titles and authors, and so on. In a language with only loops, we'd write code like this every single time we encounter this problem:
stop = [xs.size, ys.size].min
for i in 0...stop
# process xs[i], ys[i]
end
stop = [xs.size, ys.size].min for i in 0...stop # process xs[i], ys[i] end
Wouldn't it be nice if we could abstract this pattern out to a utility function that did everything but the processing? We can. The pattern is called zip, and we can implement it in an each-like fashion:
def each_zip(as, bs)
stop = [as.size, bs.size].min
for i in 0...stop
yield(as[i], bs[i])
end
end
def each_zip(as, bs)
stop = [as.size, bs.size].min
for i in 0...stop
yield(as[i], bs[i])
end
endSuppose that we wanted to produce a new array instead of achieve a side effect. Maybe we want to sum up the two arrays? Or take the larger item of each pair? Then we write a map-like zip:
def zip(as, bs)
stop = [as.size, bs.size].min
cs = []
for i in 0...stop
c = yield(as[i], bs[i])
items.push(c)
end
end
def zip(as, bs)
stop = [as.size, bs.size].min
cs = []
for i in 0...stop
c = yield(as[i], bs[i])
items.push(c)
end
endHere's how we'd form first- and last-name pairs with our zip:
firsts = %w{Scott Barb Cara}
lasts = %w{Mass Ru Mass}
joins = zip(firsts, lasts) { |first, last| "#{first} #{last}" }
firsts = %w{Scott Barb Cara}
lasts = %w{Mass Ru Mass}
joins = zip(firsts, lasts) { |first, last| "#{first} #{last}" }Again, Ruby's Array class offers a builtin zip function that we should favor over our own.
Map, Filter, and Fold
Many higher-order abstractions have already been written for us in the Ruby standard library. The most famous of these are map, filter, and fold/inject. All accept a block parameter. Let's examine each of these with a short example:
map to turn an array of integers into an array of floats.
fs = is.map { |i| i.to_f }
fs = is.map { |i| i.to_f }filter to pull from an array of words just those words that start with S.
swords = words.filter { |word| word.start_with?('S') }
swords = words.filter { |word| word.start_with?('S') }inject to find the minimum value in an array.
min = xs.inject { |smallest, x| x < smallest ? x : smallest }
min = xs.inject { |smallest, x| x < smallest ? x : smallest }Next we give you a chance to be a client of these higher-order functions. I'll present you with a collection of tasks, and you'll solve one of them with your neighbors.
Map
Use map in all answers.
-
Turn an array of words into URLs for looking up their definitions. For example:
["squalor", "dearth"]→["https://dictionary.com/browse/squalor", "https://dictionary.com/browse/dearth"]. -
Turn an array of counts into array of stars of corresponding length. For example:
[3, 1, 0]→["***", "*", ""]. -
Turn an array of ranges into an array of their counts. For example,
[25..26, -1..1]→[2, 3]. -
Halve an array of numbers. For example:
[100, 99.2, 99]→[50.0, 49.6, 49.5]. -
Round an array of numbers to nearest multiple of 10. For example:
[87, 7, 432]→[90, 10, 430]. -
Turn a hash into an array of declarative statements. For example:
{author: "Barbara Picard", title: "One is One", year: 1965}→["Author is Barbara Picard", "Title is One is One", "Year is 1965"]. There's aStringmethod to help with the case.
Filter
Use filter in all answers.
-
Keep names that have a space. For example:
["van Gogh", "Granger", "Hernandez Rivera"]→["van Gogh", "Hernandez Rivera"]. -
Keep key-value pairs where the key is shorter than the value. For example:
{"Barack": "Michelle", "Jada": "Will", "Taylor": "Travis"}→{"Barack": "Michelle"}. -
Keep numbers within [0, 100]. For example:
[-57, 20, 100]→[20, 100]. -
Keep words that are all uppercase. For example:
["NO", "Sugar", "CAN"]→["NO", "CAN"]. -
Keep ranges that contain 16. For example:
[4..25, 17..20, 11..16]→[4..25, 11..16]. -
Keep strings that have “ese” as a suffix. For example:
["Japanese", "English", Portuguese"]→["Japanese", "Portuguese"].
Fold
Use inject in all answers.
-
Double the elements of an array. For example:
[10, "smile"]→[10, 10, "smile", "smile"]. -
Report if all numbers are negative. For example:
[-3, 10]→false. -
Connect strings in an arrow sequence. For example:
["dog", "cat", "bird"]→"dog->cat->bird". -
Build an acronym. For example:
%w{you only live once}→"yolo". -
Remove all elements from
(1..10).to_a. For example:[3, 9, 4, 6, 5, 8, 7]→[0, 1, 2, 10]. -
Count the lengths of all the strings. For example:
["sheetz", "pilot", "liberty"]→18.
TODO
Here's your list of things to do before we meet next:
See you next time.
Sincerely,
P.S. Here's a haiku for you!