Operations on lists
Here’s another use for lists: extracting columns from a table. We have already
seen Row
, the data type representing rows: it needs to have a value for every
named column in the table. What about columns? Do we need a Column
data type?
A column consists of an ordered collection of values, of unbounded length. So a
column is really just a list!
We can get the list of values for a table column with get-column
:
fun order-flavors(t :: Table) -> List: all-flavors = t.get-column("flavors") L.distinct(all-flavors) end
The L.distinct
operation takes a list and returns a list of its distinct
values.
Today we’re going to talk about some operations on lists. In subsequent classes we’ll see how we can create operations like these out of more basic building blocks; for now, we’ll just see how to use and combine them. Many of these operations should remind you of similar operations on tables.
Let’s start with some lists describing recipes and ingredient types.
import lists as L # ingredients meat = [list: "chicken", "pork", "beef", "fish"] dairy = [list: "egg", "milk", "butter", "whey"] gluten = [list: "flour", "spaghetti"] # recipes pancakes = [list: "egg", "butter", "flour", "sugar", "salt", "baking powder", "blueberries"] dumplings = [list: "egg", "wonton wrappers", "pork", "garlic", "salt", "soy sauce"] pasta = [list: "spaghetti", "tomatoes", "garlic", "onion", "salt"] # stored pantry = [list: "spaghetti", "wonton wrappers", "garlic"]
All of these are lists of strings, where each string describes an ingredient.
A shopping list
Let’s say we want to go shopping for the ingredients we need to make all three dishes. How would we write code to create such a list?
meal-plan = L.append(pancakes, L.append(dumplings, pasta)) shopping-list = L.filter(lam(i): not(L.member(pantry, i)) end, L.distinct(meal-plan))
We’ve already seen the distinct
and member
functions. filter
is similar to
the filter-with
function on tables: it keeps list members on which its function
argument returns true
. append
combines two lists, adding one onto the end of
the other.
Dietary restrictions
What if we wanted to write a boolean function on recipes that returns true
if
the recipe is gluten-free? We could do so as follows:
fun is-gluten-free(recipe :: List<String>) -> Boolean: L.length(L.filter( lam(i): L.member(gluten, i) end, recipe)) == 0 end
We’re finding all of the glutenous ingredients in the recipe, and returning true
only if there aren’t any. length
just returns the number of elements of a list.
The <String>
describes the contents of the list that is-gluten-free
expects as an argument: it should be a list of strings, not (for instance) a
list of numbers.
There’s a slightly cleaner way to write this function:
fun is-gluten-free(recipe :: List<String>) -> Boolean: not(L.any(lam(i): L.member(gluten, i) end, recipe)) end
any
returns true if its function argument returns true
on any member
of its list argument, and false
otherwise. In this case, it returns
true
when any member of the recipe is glutenous–which is what we want!
We can write a similar function to check for veganism:
fun is-vegan(recipe :: List<String>): not(L.any(lam(i): L.member(meat, i) or L.member(dairy, i) end, recipe)) end
Transforming recipes
What if we want to take a recipe and make it vegan? We’ll have to write a function that both takes and returns a list, where some of the members have been transformed. None of the functions we’ve seen so far will do the job.
How would we “veganize” a single ingredient?
fun veganize-ingredient(ingredient :: String) -> String: if ingredient == "egg": "flax" else if ingredient == "pork": "mushroom" else if ingredient == "beef": "tofu" else if ingredient == "chicken": "chick'n" else if ingredient == "butter": "vegetable oil" else: ingredient end end
Now we need a way to apply this “veganizing” function to every element of a
list. We can do this with map
:
fun veganize-recipe(recipe :: List<String>) -> List<String>: L.map(veganize-ingredient, recipe) end
Operation signatures
What’s the function signature for filter
? One way to write it is
filter :: (f :: Function, l :: List) -> List
Can we get more specific? How about
filter :: (f :: (a -> Boolean), l :: List<a>) -> List<a>
a
doesn’t refer to any specific type–it’s a stand-in for any type. This tells
us a lot more about the function’s behavior, and when using it might make sense!
How about map
?
map :: (f :: (a -> b), l :: List<a>) -> List<b>