Working with Lists - Mapping functions

Search for a command to run...

No comments yet. Be the first to comment.
In this series, some more advanced features of PicoLisp will be explained that have not been covered in the beginner's tutorial.
Writing code in PicoLisp means to think a lot in lists and list structures. This is because lists are one of only three data types in PicoLisp, the other two being numbers and symbols. So, every complex construct (like stacks, functions, trees, graph...
In our previous post, we introduced how to utilize Discrete Event Simulation (DES) in PicoLisp. Now, let's explore another application: a railroad model simulation that includes tracks and trains. The visualization is ASCII-based, so feel free to unl...

The Dining Philosophers

In this post, we will introduce the concept of co-routines and how they are handled in PicoLisp. This will be the foundation for the subsequent posts on Discrete Event Simulation and finally a couple of examples, including an ASCII model railway simu...

One of the many nice things of PicoLisp is that it comes with a built-in database. This makes it easy to collect and store our own data, for example via a mobile app. But not all data is best represented in tables and lists: sometimes a colorful litt...

In the last two posts, we have built a little app that shows the current location and displays the nearest ice cream shops: https://picolisp-explored.com/an-openstreetmap-app-written-in-picolisp-pt-1 https://picolisp-explored.com/an-openstreetmap-ap...

In one of the last posts, we discussed functions to add or remove items from lists with push, pop and so on. Today, we will see how to apply functions to lists with mapping functions.
In mathematics, the term mapping is often used as a synonym for function. It means that a set X is assigned to elements of a set Y. X is also called "domain" and Y "codomain".

Now in programming, a "set" is usually a collection of data (in PicoLisp often in form of a list). These sets are transformed via functions. In order to do this in a generic way, many functional programming languages provide higher-order functions that take the function to be mapped as one of its argument.
Obviously, there are several ways to implement this specifically:
Input handling:
Function output:
The table below shows an overview over some important higher-order functions based on their input/output classification.

We will go through all four categories in detail now, to understand what each of these functions does and what the subtle differences are.
Let's start with the many-to-many functions, as these are probably the most intuitive ones.
mapcarThe typical example is mapcar: It applies a function to each element of a list. If additional arguments are given, their elements are passed to the function too.
: (mapcar + (1 2 3) (4 5 6))
-> (5 7 9)
: (mapcar + (1 2 3) 5)
-> (6 7 8)
: (mapcar '((X Y) (+ X (* Y Y))) (1 2 3 4) (5 6 7 8))
-> (26 38 52 68)
filter and extractThe second example is filter, which applies a function to each element of a list, and returns all elements where the function returns non-NIL.
: (filter num? (1 A 2 (B) 3 CDE))
-> (1 2 3)
: (filter < (2 9 3 8 4 7) (5 4 3 9 9 5))
-> (2 8 4)
Its counterpart is extract, which returns all non-NIL values of the function. Note the difference to filter in the example below:
: (setq A NIL B 1 C NIL D 2 E NIL F 3)
-> 3
: (filter val '(A B C D E F))
-> (B D F)
: (extract val '(A B C D E F))
-> (1 2 3)
mapcanmapcan applies the function to each element just like mapcar, but it returns a concatenated list of results. Observe the difference between mapcar and mapcan in the example below:
: (mapcan reverse '((a b c)( d e f)(g h i)))
-> (c b a f e d i h g)
: (mapcar reverse '((a b c)( d e f)(g h i)))
-> ((c b a) (f e d) (i h g))
byby takes two functions as argument, where the second one is typically either sort or group. It returns a sorted or grouped list of elements. Let's take the following data set "A=1, B=2, C=3":
: (setq A 1 B 2 C 3)
-> 3
# sort "C A B" by their value:
: (by val sort '(C A B))
-> (A B C)
In the second common use case of by, we call it together with group. group builds a list of lists by grouping all elements with the same CAR. Let's say we want to group a list of numbers by even and uneven values:
: (setq L (3 11 6 2 9 5 4 10 12 7 8 1))
-> (3 11 6 2 9 5 4 10 12 7 8 1)
# group by even or uneven number:
: (by '((N) (bit? 1 N)) group L)
-> ((3 11 9 5 7 1) (6 2 4 10 12 8))
Now we come to the "Many-To-One functions" that apply a function to each element of a list, but return a single value instead of a list.
mapcmapc is very similar to mapcar, but it returns only the last calculation instead of all values. View mapcar and mapc in comparison:
: (mapcar '((X Y) (+ X (* Y Y))) (1 2 3 4) (5 6 7 8))
-> (26 38 52 68)
: (mapc '((X Y) (+ X (* Y Y))) (1 2 3 4) (5 6 7 8))
-> 68
applyAnother example is apply:
: (apply + (1 2 3))
-> 6
: (apply * (5 6) 3 4)
-> 360
: (apply '((X Y Z) (* X (+ Y Z))) (3 4 5))
-> 27
Note: One could argue that apply doesn't apply a function to each element, but rather passes the list as arguments to a function. In this case it is rather a "one-to-one" mapping. I leave it up to you how you judge it.
find and pickSimilarly to filter/extract, we can return a single element from a dataset based on certain criteria with find. It returns the first element that fulfills the requirement and then stops the execution.
: (find '((A B) (> A B)) (1 2 3 4 5 6) (6 5 4 3 2 1))
-> 4
: (find > (1 2 3 4 5 6) (6 5 4 3 2 1)) # shorter
-> 4
pick works similarly to extract, as it returns the value of the function that fulfills the requirement. find and pick in comparison:
: (setq A NIL B 1 C NIL D 2 E NIL F 3)
-> 3
: (find val '(A B C D E))
-> B
: (pick val '(A B C D E))
-> 1
fully, cnt, maxi, mini, sumMore similar functions (you can tell the function from the name, I guess): fully, cnt, maxi, mini, sum. Below you can find some examples:
: (fully gt0 (1 2 3))
-> T
: (fully gt0 (1 -2 3))
-> NIL
: (cnt cdr '((1 . T) (2) (3 4) (5)))
-> 2
: (setq A 1 B 2 C 3)
-> 3
: (maxi val '(A B C))
->C
: (mini val '(A B C))
-> A
: (sum val '(A B C))
-> 6
Up to now, all functions have always been applied to each element of the provided list. Now we will discuss two functions that instead apply the function to the list as a whole and return a new list as result.
mapconmapcon applies a function to a list, and to all successive CDRs. Then all results are concatenated.
: (mapcon copy '(1 2 3 4 5))
-> (1 2 3 4 5 2 3 4 5 3 4 5 4 5 5)
First the whole list (1 2 3 4 5) is copied, after that its CDR (2 3 4 5) and so on. Observe the difference between mapcon and mapcan:
: (mapcan reverse '((a b c)( d e f)(g h i)))
-> (c b a f e d i h g)
: (mapcon reverse '((a b c)( d e f)(g h i)))
-> ((g h i) (d e f) (a b c) (g h i) (d e f) (g h i))
mapcan reverses each single list item, while mapcon reverses the list as a whole, and after that each CDR successively. Both functions concatenate the result to a new list.
maplistSimilarily, maplist also applies a function to a list as well as to all successive CDRs, but without concatenating the result:
: (maplist cons (1 2 3) '(A B C))
-> (((1 2 3) A B C) ((2 3) B C) ((3) C))
: (maplist reverse '((a b c)(d e f) (g h i)))
-> (((g h i) (d e f) (a b c)) ((g h i) (d e f)) ((g h i)))
seekAnother interesting example is seek, which applies a function to a list and its CDR until non-NIL is returned:
: (seek '((X) (> (car X) 9)) (1 5 8 12 19 22))
-> (12 19 22)
mapFinally, we have the plain map function. Like maplist and mapcon, it applies the function to list and all the successive CDRs, but returns only the result of the last application.
: (map println (1 2 3 4) '(A B C))
(1 2 3 4) (A B C)
(2 3 4) (B C)
(3 4) (C)
(4) NIL
-> NIL
map function groupAs you might have noticed, there are six functions with very similar names: map, mapc, maplist, mapcar, mapcon, mapcan. Instead of the relations above, a better way to classify them might be by argument and return value:
Argument Return
List CAR Value
------------+----------+---------
map mapc | last
maplist mapcar | list
mapcon mapcan | conc
While map, maplist and mapcon take the whole list as argument, mapc, mapcar and mapcan iterate through the list and use each CAR. The return value is either the last element, a list or a concatenated list.
All the functions above can be emulated through these functions, and in fact, all map- functions can be replaced by mapcon only.
Examples: Emulate mapcar by mapcon:
(mapcar inc (1 2 3))
(mapcon '((L) (list (inc (car L)))) (1 2 3))
Emulate maplist by mapcon:
(maplist '((L) (+ (car L) (cadr L))) (1 2 3))
(mapcon '(((A B)) (list (+ A B))) (1 2 3))
Emulate filter by mapcan:
(filter foo Lst)
(mapcan '((X) (and (foo X) (list X))) Lst)
While some of these functions have really specific uses cases (like by), other functions are widely used like mapcar, filter and so on. I hope this post helped to sort the different types of mapping functions.
We will see some interesting use cases of by and mapcon in the next Euler riddle.