Map, filter and friends
The built-in helpers that replace most hand-written walks: map to change every value, filter to keep some, apply to spread a list into a call.
Say what you want, not how to walk
A hand-written walk down a list makes the reader follow a counter and a base case before they learn what the function is for. map and filter remove that noise: you hand over a rule, and they take care of the walking.
(map f xs) applies f to every value and collects the answers into a new list of the same length. (filter keep? xs) keeps only the values the rule approves of, so the list can come out shorter. Neither changes the list you gave it — a new list is built and the old one is untouched.
Two more, and the shape of a pipeline
(apply f xs) calls a function with the values of a list as its arguments, which is how (apply + prices) adds a whole list. for-each is the cousin of map for when you only want the effect: it runs the rule on every value and gives nothing back.
Put map and filter one after the other and you have a pipeline: change every value, keep the ones that pass, then total what is left. That reads like the sentence you would say out loud, and it is why these helpers show up in almost every Scheme program you will meet.
A rule is a function you do not have to name
map and filter take a function as their first argument. Most of the time the function is used once, so writing it on the spot with lambda keeps the rule next to the list it applies to.
The rule has to accept one value and answer one value. For filter the answer is read as a yes or no: anything except #f keeps the value, including 0 and the empty list.
A named rule is worth writing when you use it twice or when the name explains the intent. cheap? says more at the call site than the same comparison written inline.
Pipelines, and the order of the nesting
A pipeline is a stack of calls: the innermost runs first. (apply + (map price (filter in-stock? items))) first keeps the items in stock, then turns each into a price, then adds them.
Read it from the inside out, even though the sentence you would say runs left to right. Each helper returns a new list, so the next one has something to walk.
apply is the bridge at the end: it spreads a list into a call that expects separate arguments. for-each looks similar to map but is for the effect, and it gives nothing back to the pipeline.
A rule, handed to a helper
(define words (list "a" "bee" "see" "dee"))
(print (map string-upcase words))
(print (filter (lambda (w) (> (string-length w) 1))
words))
(print (map string-length words))
(print (apply + (list 1 2 3 4)))
; for-each uses each value and gives nothing back.
(for-each (lambda (w) (display w) (display "-"))
words)
(newline)
(print (member "see" words))
(print (member "zed" words))A helper is only as good as its rule
map does not care what the rule does, only that it takes one value and answers one value. A rule that answers a list gives a list of lists, which is legal and usually not what you wanted. A rule that prints answers something unspecified, so the result is a list of those values rather than your text.
Write the rule as if it were the only thing the reader will see. (lambda (p) (> p 10)) says keep the prices above ten; a rule with three nested conditions says the author was thinking about something else. If the rule needs a name, give it one.
The pipeline reads from the inside
The innermost call runs first, even though the sentence in your head runs left to right. (apply + (map price (filter in-stock? items))) filters, then maps, then adds. Reading it from the inside out is the only reliable way to predict the result.
Each step returns a new list, so the next one has something to walk. That is why the pipeline is safe: no step can change the list another step is holding, and the original data is still there if you need it.
Three things that surprise people. member answers the tail of the list, not #t, so it is true but not a boolean. apply needs a list as its last argument, not a single value. And a rule passed to map should not print, because the output appears once per value and rarely means what you intended.
Try these next
- Filter the words longer than three letters, then uppercase what is left.
- Use
applywithmaxon a list of numbers. - Compare
(map print xs)with(for-each print xs)and read what each answers.
Changing and choosing
Run it and compare the two halves: the prints at the top change values and keep values, and the two at the bottom do the same work with a named rule. Then write a pipeline that keeps the prices above 10 and totals them.
A pipeline over words
Run it and compare the two filtered lists. Then add a rule that keeps words ending in a, and use it with both `filter` and `map`.
member answers with the tail of the list from the value onwards, not a yes or no: (member 7 prices) gives (7 20). That makes it useful for the values still to come, and the test is simply whether the answer is a list at all. assoc, in the next chapter, answers in the same style.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
If xs has five values, how many are in (map f xs)?
