CS 334: Lab 5: Folding Fun

Overview

In this lab, you will explore the fold higher-order functions in ML and use them to write a variety of list-processing functions concisely.

You may work with a partner on this lab if you like, but it is not required.

Getting Started

Your GitLab account will have a "lab5" project. You can follow the same instructions as on Lab 1 for cloning it and (optionally) adding a partner.

Programming Problems

The "fold-left" (and "fold-right") functions appear in many languages (as reduceRight/Left in Javascript, as accumulate in C++, as foldl/foldr in ML, and so on.)

Here are their definitions in ML:

fun foldr f v nil =     v
  | foldr f v (x::xs) = f (x, foldr f v xs);

fun foldl f v nil =     v
  | foldl f v (x::xs) = foldl f (f(x, v)) xs;

Thus, foldr g b [a_0, ..., a_n] computes

g(a_0, g(a_1, g(a_2, ... g(a_{n}, b) ... )))

and foldl g b [a_0, ..., a_n] computes

g(a_{n}, g(a_{n-1}, g(a_{n-2}, ..., g(a_{0}, b) ... )))

The "fold-right" function reduces the elements in a list to a single value by repeated application of g, starting at the right of the list and working to the left. The "fold-left" function starts from the left and works to the right.

Here is an example usage, which defines a function sum that adds together the numbers in a list:

- fun add(x,y) = x+y;
- fun sum elems = foldr add 0 elems;
- sum [2,3,4];
val it = 9: int

In effect, sum [2,3,4] computes

add(2, add(3, add(4, 0)))

Writing that function recursively would give us:

fun sum_rec nil = 0
  | sum_rec (x::xs) = x + sum_rec(xs);

which computes the exact same value: sum_rec [2,3,4] computes 2 + (3 + (4 + 0)). Many computations involve traversing a list and computing a "summary" value for it. We explore other examples below, and our folding operations enable us to write them in a succinct, elegant way.

We could also define sum using foldl:

- fun sum2 elems = foldl add 0 elems;

in which case sum2 [2,3,4] computes

add(4, add(3, add(2, 0)))

Of course, we typically combine folding with anonymous functions, as in the following definition of sum:

- fun sum elems = foldr (fn (x,result) => (x+result)) 0 elems;

The type of both foldr and foldl is

('c * 'd -> 'd) -> 'd -> 'c list -> 'd

That is, it takes as parameters a reducing function, an initial value, and a list. It produces a single summary value.

  1. Using a fold operation, write a function concatWords: string list -> string. This function should return return a string with all strings in the list concatenated:

    - concatWords nil;
    val it = "" : string
    - concatWords ["Three", "Short", "Words"];
    val it = "ThreeShortWords" : string
    
  2. Using a fold operation, write a function wordsLength: string list -> int. This function should return the total length of all words appearing in a list of strings. For example:

    - wordsLength nil;
    val it = 0 : int
    - wordsLength ["Three", "Short", "Words"];
    val it = 15 : int
    
  3. Can we always use foldl in place of foldr? If yes, explain. If no, give an example function f, list l, and initial value v such that foldr f v l and foldl f v l behave differently.

  4. Using a fold, write a function count: ''a -> ''a list -> int. It computes the number of times a value appears in a list. For example:

    - count "sheep" ["cow", "sheep", "sheep", "goat"];
    val it = 2 : int
    - count 4 [1,2,3,4,1,2,3,4,1,2,3,4];
    val it = 3 : int
    
  5. Using a fold, write a function partition: int -> int list -> int list * int list that takes an integer p and a list of integers l, and that returns a pair of lists containing the elements of l smaller than p and those greater than or equal to p. The ordering of the original list should be preserved in the returned lists. (We wrote a recursive form during lecture as part of quicksort.)

    - partition 10 [1,4,55,2,44,55,22,1,3,3];
    val it = ([1,4,2,1,3,3],[55,44,55,22]) : int list * int list
    
  6. Using a fold, write a function poly: real list -> (real -> real) that takes a list of reals c[a_0, a_1, ..., a_{n-1}] and returns a function that takes an argument b and evaluates the polynomial

    a_0 + a_1 x + a_2 x^2 + \cdots + a_{n-1} x^{n-1}

    at x = b; that is, it computes \sum_{i=0}^{n-1} a_i b^i. For example,

    - val g = poly [1.0, 2.0];
    val it = fn: real -> real
    - g(3.0);
    val it = 7.0: real
    - val g = poly [1.0, 2.0, 3.0];
    val it = fn: real -> real
    - g(2.0);
    val it = 17.0: real
    

    Hint

    a_0 + a_1 x + a_2 x^2 + a_3 x^3 = a_0 + x (a_1 + x (a_2 + x a_3)). This is an example of Horner's Rule. Horner's Rule demonstrates that we can evaluate a degree n polynomial with only O(n) multiplies.

Submitting Your Work

Submit your code to the GradeScope assignment named, for example, "Lab 1". You can submit in one of two ways:

  • Upload files: Click "Upload" and select all of your source files, or
  • Link GitHub: Click "GitHub" and select your repository and branch.

Please do not change the names of the starter files. Also:

  • If you worked with a partner, only one of each pair needs to submit the code.
  • Indicate who your partner is when you submit. Specifically, after you upload your files, there will be an "Add Group Member" button on the right of the Gradescope webpage -- click that and add your partner.

Autograding: Gradescope will run an autograder on your code that performs some simple tests. Be sure to look at the autograder output to verify your code works as expected. We will run more extensive tests on your code after the deadline.