Map and filter are two built-in looping constructs in Scheme; both consume a function and a list, but each performs a different operation on the list. Filter, as its name suggests, creates a list of all elements on which the function returns true, while map calls the given function on every element of the list, returning a new list of the results.
As a reminder, here are their definitions:
;; filter : (alpha -> boolean) list[alpha] -> list[alpha] ;; extracts elements of list that satisfy predicate (define (filter pred alst) (cond [(empty? alst) empty] [(cons? alst) (cond [(pred (first alst)) (cons (first alst) (filter pred (rest alst)))] [else (filter pred (rest alst))])])) ;; map : (alpha -> beta) list[alpha] -> list[beta] ;; returns list of results from applying function to each element ;; of the input list (define (map f alst) (cond [(empty? alst) empty] [(cons? alst) (cons (f (first alst)) (map f (rest alst)))]))
Okay, but what's interesting about map and filter? Why are they any better/worse/different from loops in other languages?
They accept functions as arguments, which is different from the loops you've written before. Remember: functions are just a way of giving names to computations. Map and filter simply take the computation to do in the loop as an argument. Using map and filter makes your code cleaner, because you don't interleave the code for running the loop with the code that you perform at each step of the loop.
Think about a usual while for processing a loop in C++: you have to write down both the code to walk along the list and check whether you're at the end of the list, as well as the code that you want to perform on each element. Map and filter capture the common parts of walking down the list into one function, leaving you to write only the part that is different for your particular loop (as a function).
The names of the looping operators tell you something about the computation performed. If you see filter in your code, you know you are trying to eliminate/keep certain elements. If you see map, you know you are trying to modify all of the elements. They go a long way to making programs more readable.
Map and filter demonstrate an important attribute of Scheme: you can define your own loops!. If I give you a problem that requires a different kind of loop than map or filter, you can write a function (your new looping construct) that does the kind of traversal you need and takes a function for the work to do within the traversal. You can define loops over trees, arrays, or any other data structure that you create. This is very useful!
In software engineering terminology, these loops over particular data structures are called iterators -- defining iterators goes a long way to improving program design, and most modern languages give you some facilities for defining iterators. Some are more cumbersome than others, though. Scheme's is pretty lightweight, once you get the hang of it.
The text has a lot more information on building custom iterators and the process by which you define an iterator for particular pairs of functions. See Chapter 22 of HtDP for this material.
Imagine a travel agency that maintains two databases: one of information about cities and one about flights between cities.
;; A city is a (make-city symbol list[symbol]) (define-struct city (name features)) (make-city 'new-york (list 'music 'museums 'baseball 'strong-accents)) ;; A flight is a (make-flight symbol symbol number list[number]) (define-struct flight (from to number dates)) (make-flight 'boston 'new-york 675 (list 10 13)) ;; this example says that flight 675 from boston to new-york ;; operates on the 10th and 13th of each month
Assume you have lists of cities and flights. Using these lists, you can write several functions to help implement a travel agency. As you work through these exercises, ask yourself which ones can be written with map/filter, which ones cannot, and which ones require nested lambdas instead of externally-defined functions. If you can write these and know why you wrote them as you did, you're in good shape with map and filter.
Assume that flights is a list of flight structs and cities is a list of city structs. Give an English description of what each of the following expressions computes:
(map flight-number (filter (lambda (flt) (symbol=? (flight-to flt) 'austin)) flights))
(filter (lambda (days) (> (length days) 3)) (map flight-dates flights))
Write a function dates-to
which takes the names of
two cities (home and destination) and returns a list of dates when one
can fly from the home city to the destination.
Write destinations-by-date
which takes a city name
(the home city) and a date (number) and returns a list of all the city
names that one can fly to on the given date from the given home city.
Write cites-by-feature
which takes a list of cities
and a feature and returns a list of all the city names with that
feature.
Write destinations-by-date-feature
which takes a
city name, date, and a feature and returns a list of all the city
names that have the given feature and that one can fly to on the given
date.
Write cites-by-some-feature
which takes a list of
cities and a list of features and returns a list of all the city names
that have at least one of the features.
You can make up a whole host of other functions to write for practice. Be creative!