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?