GMap is an iteration macro for Common Lisp. It was conceived as a generalization of mapcar
(hence the name). It is intended for cases when mapcar doesn’t suffice because the things
you want to map over aren’t in lists, or you need to collect the results of the mapping into
something other than a list.
That is, gmap is probably the right thing to use when you are using iteration to perform the
same computation on each element of some collection, as opposed to changing your state in some
complicated way on every iteration of a loop. It’s conceptually reasonable to imagine all the
iterations of a gmap as happening in parallel, just as you might with mapcar. It also
supports arbitrary reductions of the results of the mapped function; more on this below.
GMap is explicitly intended only for iterations that fit this "map-reduce" model. It is not trying
to be a "universal" iteration construct. People have asked me what guarantees it offers concerning
the ordering of the various operations it performs, and the answer is none, other than those
obviously imposed by the data flow (a result can’t be used until it is computed). I think that the
question was rooted in experience of people using loop or other iteration constructs and
supplying variable update expressions with side effects, so that there was "crosswise" data flow
between the iterations. I strongly advise that such side effects be avoided in gmap calls.
If you find yourself wanting to use them, either there is a better way, or else gmap simply
isn’t the right tool for the job. In short, you should think of gmap very much as a
functional iteration construct.
In general, my philosophy about iteration in Lisp is that there are many ways to do it, for the very
good reason that there are many kinds of iterations, and one should use the tool appropriate for the
particular case one is confronted with. So, for example, I almost never write a gmap form
with side effects. If I’m just iterating over a list for effect, I’ll use dolist. For
iterations with cross-iteration data flow ("loop-carried dependences" is the compiler developer’s
term) or where the iteration space is not well defined in advance, I might use good old do,
or I might even write the code tail-recursively, counting on the fact that most CL implementations
these days do tail-call optimization at least on local calls (calls to functions defined by
labels or flet).
So when I do use gmap, it’s specifically intended to convey to someone reading the code that
the iterations are independent — either the function being mapped is side-effect-free, or at the
very least, the argument generators and result collectors are mutually independent. I strongly urge
adherence to this rule, whose meaning will become clearer as we proceed.
Even with that constraint, I find that occasions to use gmap are not at all uncommon. It has
proven handy over the years, and FSet makes substantial internal use of it.
Common Lisp has cl:map, which already generalizes mapcar somewhat: it accepts vectors
as well as lists, and takes a type specifier to control the type of the result collection, in case
you want to construct a vector or string. (I’m using an explicit cl: prefix on map to
avoid confusion with FSet’s map.) GMap takes this idea even further by adding a little
syntax, to make the argument generators and result collectors extensible. For example, these three
expressions are equivalent:
(mapcar #'foo this-list that-list) (cl:map 'list #'foo this-list that-list) (gmap (:result list) #'foo (:arg list this-list) (:arg list that-list))
But along with being able to generate arguments (for the function being mapped; #'foo in this
example) from lists or vectors, GMap has many other argument generators, e.g., index for an
integer subrange; hash-table for the pairs of a hash table; or file-chars or
file-lines for the contents of a file. Similarly, GMap has more result collector types,
e.g., and (provides the functionality of cl:every), or (ditto for
cl:some), sum, max/min, etc.
FSet builds on these by adding argument generators and result collectors for its collections. (GMap predated FSet and can be used without it; I will list the “base” GMap features separately from those added by FSet.)
Like mapcar, gmap accepts any number of argument clauses; the iterations they specify
occur in parallel, and the iteration terminates when any of them is exhausted.
GMap makes use of multiple values: some argument types generate two arguments for each call to the function being mapped instead of one, and some result types expect the function being mapped to return two values. For example, this expression converts a plist pl to an alist:
(gmap (:result list) #'cons (:arg plist pl))
This one converts it to a hash table:
(gmap (:result hash-table) #'values (:arg plist pl))
The :arg plist clause emits the pairs of the plist; they become two arguments to the function
being mapped. In the first case, this is cons, and a list is formed of the resulting conses.
In the second case, values reflects the two arguments back as two values, and then the
:result hash-table clause takes those pairs and inserts them into a hash table.
(GMap takes the view that consing up a collection of function results is a kind of reduction — a slightly unusual view, perhaps, but not unreasonable. So it treats collecting the results into a list, for example, as an instance of the same pattern as, say, summing them.)
If you just want to pass the argument(s) to the result collector, there are a few ways to say this
(too many, arguably). The above example uses #'values, which is general. GMap will also
interpret the symbol :id as equivalent to #'values; you can also use nil for
this, though I wouldn’t really recommend it — it’s rather cryptic. Finally, if only one value is
involved, you can use #'identity.
To get the best performance out of your gmap forms, make the argument types as specific as
possible. For instance, if you know the collection you want to iterate over is a CHAMP set, use
argument type ch-set rather than set or the very generic sequence.
GMap is extensible — you can add your own argument and result types — but I won’t go into that here; see the source.
In sum — I know, there are other iteration macros out there that you’re already familiar with; you may be understandably unenthusiastic about learning another one. It’s certainly not necessary to use GMap along witih FSet. But the two do fit together nicely, and GMap has a simple and familiar conceptual model, as I’ve just explained. I hope you’ll give it a try.