Mapping Instead of Iteration

You can think of a mapping function as a kind of special purpose iterator.  Every mapping function expects you to supply a function.  A typical mapping function applies your function to every element of the supplied list(s).  One variation on this theme applies your function to successive sublists.

A sequence is a generalization of the list data type.  Vectors (one-dimensional arrays) and lists are specializations of the sequence data type.  Some mapping functions work only with lists as inputs, while others accept sequences.

MAPCAR, MAPC, and MAPCAN process successive list elements

The first group of mapping functions processes successive elements of lists.  The mapping functions in this group differ in how they construct a return value.

MAPCAR processes successive elements of one or more supplied lists.  You must supply a function that accepts as many arguments as the number of lists you supply to MAPCAR, which applies your function to successive elements and combines the function's results into a freshly constructed list.  The mapping stops upon reaching the end of the shortest list; MAPCAR's result has as many elements as the shortest input list.

MAPC does not combine the results of applying your function to successive elements of the input list(s).  Instead, it processes the inputs just for effect, and returns the first input list as the result of MAPC.

MAPCAN combines results using the destructive function NCONC.  Since NCONC -- like its nondestructive counterpart APPEND -- expects its arguments to be lists, the function you supply to MAPCAN must always return a list.

? (mapcar  #'atom (list 1 '(2) "foo" nil))
(T NIL T T)
? (mapcar  #'+ (list 1 2 3) (list 4 5 6))
(5 7 9)
? (mapc #'(lambda (x y) (print (* x y))) (list 1 0 2) (list 3 4 5))
 
3
0
10
(1 0 2)
 
? (mapcan #'list (list 1 2 3) (list 4 5 6))
(1 4 2 5 3 6)
 
? (mapcan #'(lambda (a b) (list (cons a b))) (list 1 2 3) (list 4 5 6))
((1 . 4) (2 . 5) (3 . 6))

MAPLIST, MAPL, and MAPCON process successive sublists

MAPLIST processes successive sublists of one or more supplied lists.  You must supply a function that accepts as many arguments as the number of lists you supply to MAPLIST, which applies your function to successive sublists and combines the function's results into a freshly constructed list.  The mapping stops upon reaching the end of the shortest list; MAPLIST's result has as many elements as the shortest input list.

MAPL does not combine the results of applying your function to successive sublists of the input list(s).  Instead, it processes the inputs just for effect, and returns the first input list as the result of MAPL.

MAPCON combines results using the destructive function NCONC.  Since NCONC -- like its nondestructive counterpart APPEND -- expects its arguments to be lists, the function you supply to MAPCON must always return a list.

? (maplist #'list (list 1 2 3) (list 4 5 6))
(((1 2 3) (4 5 6)) ((2 3) (5 6)) ((3) (6)))
? (mapl #'(lambda (x y) (print (append x y))) (list 1 0 2) (list 3 4 5))
 
(1 0 2 3 4 5) 
(0 2 4 5) 
(2 5) 
(1 0 2)
 
? (mapcon #'list (list 1 2 3) (list 4 5 6))
((1 2 3) (4 5 6) (2 3) (5 6) (3) (6))

MAP works on sequences, not just lists

A sequence is either a list or a vector (a one-dimensional array).  The previous group of mapping functions (MAPCAR et al) processes successive CARs or CDRs of their input lists.  MAP processes successive elements of its input sequences.

MAP requires that you specify the type of its result using one of the following designators:

Designator   Result
----------   ------
NIL          NIL
'LIST        a list
'VECTOR      a vector

Note that you can also specify subtypes of LIST or VECTOR -- your Lisp implementation may be able to optimize the storage of the result based on the type you specify.

? (map nil #'+ (list 1 2 3) (list 4 5 6))
NIL
? (map 'list #'+ (list 1 2 3) (list 4 5 6))
(5 7 9)
? (map 'vector #'+ (list 1 2 3) (list 4 5 6))
#(5 7 9)
? (map '(vector number 3) #'+ (list 1 2 3) (list 4 5 6))
#(5 7 9)

Mapping functions are good for filtering

A filter passes some of its inputs through to its output, and drops others. We can use mapping functions to implement filters by taking note of the behavior of APPEND:

? (append '(1) nil '(3) '(4))
(1 3 4)

The NIL argument (which is equivalent to the empty list) simply "disappears" from the result.  This is the key observation that we need to construct a filter.  We'll use MAPCAN to map over our input list(s) and supply a mapping function that:

·                     makes a list of each result we wish to retain in the output, or

·                     returns NIL in place of each input we wish to exclude from the output.

? (defun filter-even-numbers (numbers)
    (mapcan #'(lambda (n) (when (evenp n) (list n))) numbers))
FILTER-EVEN-NUMBERS
 
? (filter-even-numbers (list 1 2 3 4 5 6 7 8))
(2 4 6 8)

WHEN returns NIL if the condition is NIL.  We could have written (if (evenp n) (list n) nil) instead.

Here's a slightly more complex filter that returns a list of evenly divisible pairs of numerators and denominators:

? (defun filter-evenly-divisible (numerators denominators)
    (mapcan #'(lambda (n d)
                (if (zerop (mod n d))
                  (list (list n d))
                  nil))
            numerators denominators))
? (filter-evenly-divisible (list 7 8 9 10 11 12)
                           (list 1 4 5  5  2  3))
((7 1) (8 4) (10 5) (12 3))

It's better to avoid mapping if you care about efficiency

Most Lisp systems will generate more efficient code to call a function that is known during compilation than a function that can change at run time.  Mapping functions accept a functional argument, and most compilers will generate code that supports run time function binding -- even if you specify a "constant" function, such as #'+.  Also, the run time call may incur extra overhead to generate a list of arguments for the function's application.

Therefore, if you are concerned about efficiency you should write map-like functions using iteration instead of mapping functions.  But do this only when you are sure that efficiency is an issue for the portion of the program you intend to rewrite.

Predicate mapping functions test sequences

Sometimes you may need to apply a test to some input sequences and return a truth value based upon what the test returned for all of the inputs.  For example, you might want to know whether any number in a sequence is outside of a specified range, or whether every word is at least five letters long.  You could construct these tests from the mapping functions described above, but that would be more verbose (and less efficient) than using the predicate mapping functions provided by Lisp.