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>