Operations on lists

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-by 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 length 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:
  L.all(lam(i): not(L.member(gluten, i)) end, recipe)
end

all returns true if its function argument returns true on all of the members of its list argument, and false if any do not. In this case, it returns false 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>):
  L.all(lam(i): not(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>

Lists and tables

We’ve seen above one way of describing a set of recipes: as a set of hardcoded lists. This might make sense when we have a small set of recipes that doesn’t change often, but we might want something better. Here are two representations:

recipes1 = table: name, spaghetti, milk, 
  tomatoes, onions, blueberries, garlic, salt
  row: "pasta", true, false, true, true, false, true, true
end

recipes2 = table: name, ingredients
  row: "pasta", [list: "spaghetti", "tomatoes", "garlic", "onion", "salt"]
end

Which is better?