Class summary: Higher-Order Functions
Copyright (c) 2017 Kathi Fisler
1 Functions as Arguments
We looked at two programs, get-long-wordsand get-square-shapes to see that sometimes entire programs are similar except for some local computation. We can create helper functions that take the "local computation" as an input by passing functions as inputs, just as we have been passing numbers, lists, and tables.
In class, we worked this out manually by creating keep, a function that captured the common parts of our two initial functions.
fun keep(lst :: List<Any>, to-keep :: (Any -> Boolean)) -> List<Any>: |
doc: "return items from list on which to-keep function returns true" |
cases (List) lst: |
| empty => empty |
| link(f, r) => |
if to-keep(f): |
link(f, keep(r, to-keep)) |
else: |
keep(r, to-keep) |
end |
end |
end |
We then recreated get-square-shapes by writing a separate function that checks whether an image is square in its dimensions and passing it to keep:
fun is-square(ashape :: Image) -> Boolean: |
doc: "determine whether a shape has the same width and height" |
image-width(ashape) == image-height(ashape) |
end |
|
fun get-square-shapes2(shapes-list :: List<Image>) -> List<Image>: |
doc: "produce list of square shapes from given list" |
keep(shapes-list, is-square) |
end |
We also introduced the idea of local function definitions when converting get-long-words to use keep:
fun get-long-words2(word-list :: List<String>, cutoff :: Number) -> List<String>: |
|
# a function that returns bool indicating whether to keep element from list |
fun is-long(word :: String) -> Boolean: |
string-length(word) > cutoff |
end |
|
# call keep, giving it the list and the function |
keep(word-list, is-long) |
end |
Could we move is-long outside of get-long-words2? What happens if we try? Do you see the issue?
1.1 Filter
Finally, we noted that keep is such a useful concept that it is already built-in. Pyret (and many other languages) call it filter. The arguments are swapped compared to our keep function, but the idea is the same:
fun get-square-shapes3(shapes-list :: List<Image>) -> List<Image>: |
doc: "produce list of square shapes from given list" |
filter(is-square, shapes-list) |
end |
1.2 map
The other two functions from our starter file (which we did not use in class) set up a similar exercise for a function called map, which takes a function and a list and produces a list of the results of running the function on each element of the list. Example:
fun tip-left(shape :: Image) -> Image: |
rotate(25, shape) |
end |
|
# produce list of images1 shapes rotated left |
map(tip-left, images1) |
2 Anonymous Functions (Optional)
The nested function definition in get-long-words2 feels a bit clunky – we are introducing a function just to pass it as an argument, but we don’t plan to use the function anywhere else (since it’s local). Much of the overhead comes from naming the function, which isn’t all that relevant since the name doesn’t really get used (after we do substitution into keep or filter).
Languages that provide map and filter tend to also provide a notion of anonymous functions, a lightweight notation for creating functions for one-time use. Here’s an example:
fun get-long-words4(word-list :: List, cutoff :: Number) -> List: |
filter(lam(word :: String): string-length(word) > cutoff end, |
word-list) |
end |
lam (short for lambda, the traditional name for a construct that creates functions) says "make a function with parameter word" (and the body that follows). With lam, writing functions like get-long-words feels much lighter, embracing the benefits of functions like filter that accept functions as arguments.
3 What to Take from This Segment
I’d expect that you took different things from that discussion depending on your comfort level with programming:
If you are still getting comfortable with functions, simply take away two ideas: (1) helper functions can capture large chunks of code, and (2) it is okay to pass functions as arguments.
If you’re comfortable with functions, you should have gotten a sense of how to create your own functions that take functions as inputs. Ideally, you should also be able to trace a function like that and see how the pieces fit together to reproduce the original code.
filter: takes a list and a function from list elements to boolean, keeping those list elements on which the function returns true.
map: takes a list and a function from list elements to some other type. Produces a list of the results of running the function on each element from the input list in order.
Note that map and filter have analogues in tables: filter is like sieve and map resembles transform.
It will be up to you whether you choose to use filter and map in subsequent assignments. Those heading into Data Science should practice with these operations, as they tend to leave your code easier to read. For others, if you aren’t ready to work with these functions yet, that’s fine.