Programming with
Data Structures and Algorithms


In this assignment we'll explore two variations on parallelism. In the first, we'll define and use a data structure that is better suited to parallel decomposition of tasks. In the second, we'll implement and use the essence of Google's parallel computing primitive, MapReduce.

Join Lists

Lists, as you've seen them until now, permit only sequential access. You can't get to the third element until you've been through the first two. This means that when you recur, you do so on a list that is only one element smaller. If you could recur on sub-lists that were significantly smaller (by half, say), and could run each recursive call on a different processor core, you might obtain significant speedup. That requires a different data structure, which is what we're going to explore.


By downloading join-lists-support.rkt, and then requiring it you will have access to:

Join-List, our new data-structure, defined as follows:

(define-type (Join-List 'a)
  [one (elt : 'a)]
  [join-list (lst1 : (Join-List 'a)) (lst2 : (Join-List 'a)) (size : number)])

Note: just like normal Racket lists can contain elements of any type, so can join lists.

Due to the nature of the assignment, you are limited to using ONLY the following functions from the support code:

Note: type-case is allowed as long as you do not access the fields of a multiple element join-list.


When testing your solutions, you will need to compare two (Join-List 'a)s. However, we have introduced a new type and check-expect cannot check equality of (Join-List 'a)s. You can either use join-list=? to compare two (Join-List 'a)s or you can use join-list->list to convert a (Join-List 'a) into a regular Racket list. You may also find it tedious to construct larger (Join-List 'a)s by explicitly making singletons and joining them together. Instead, you may use the list->join-list procedure to construct larger (Join-List 'a)s.

Important: While you are free to use list->join-list and join-list->list for testing and debugging, you may not use either of these procedures in your solutions in any way!

The Problems

With the API above, your task is to write the following functions for (Join-List 'a)s.

Note: It may be tempting to write j-first and j-rest, then implement the other functions with j-first and j-rest as you would for normal Racket lists. The functions you write for (Join-List 'a)s should take advantage of the unique structure of (Join-List 'a)s. It may be necessary to use j-first and j-rest in some situations, but you should use split instead where possible.

Also note: While we are not grading on the efficiency of your code per-se, if your implementation is extremely inefficient, that is probably a sign that you are not using the structure of (Join-List 'a)s properly, and you might want to rethink your implementation.



For each j-function that you write (j-max, j-length...), you also need to write a corresponding oracle. In your oracles, you are allowed to use join-list->list and list->join-list. You should consider how you can reuse code for several of your Oracle functions.


For the second part of this assignment, you will have to implement and then use your own version of Google's MapReduce. One of the things you will discover is that their operator is horribly mis-named: it isn't the composition of map and reduce. We'll anyway persist with the name, though we'll spell it “map-reduce” since ours has some small differences.

The map-reduce pattern allows you to divide problems into a series of independent, equivalent tasks that can be parallelized. Specifically, all of the map tasks can be run at the same time, as can all of the reduce tasks, because the results of each task does not depend on any of the other tasks.

First, you will have to write a map-reduce framework. This framework will take a mapper and a reducer, along with input. It will apply the mapper to each input, collect the results, and pass them to the reducer. Require map-reduce-support.rkt to get access to the following data structure:

(define-type: (tv-pair 'a 'b)
  [tv (tag : 'a) (value : 'b)])

This represents a tagged-value, the fundamental unit of data for all map-reduce programs.

For your framework, you'll need to define:

(map-reduce (input : (listof (tv-pair 'a 'b)))
            (mapper : ((tv-pair 'a 'b) -> (listof (tv-pair 'm 'n)))
            (reducer : ((tv-pair 'm (listof 'n)) -> (tv-pair 'm 'o))) : (listof (tv-pair 'm 'o))

Note that the mapper function produces a (listof (tv-pair 'm 'n)), while the reducer function takes a (tv-pair 'm (listof 'n)). You will have to figure out how to collect all of the 'ns for each 'm so you can pass them to a reducer task.

Let's consider this contract in terms of a concrete example. To help you with testing, we provide the mapper and reducer for one example: word count. In this case, 'a is a string containing the name of a file and 'b is a string containing the contents of a file. 'm is a string containing a word, 'n is an integer representing the frequency with which a word occurs in a single file, and 'o is an integer that represents the frequency with which a word occurs in all files. More specifically, the support code provides:

Once you're satisfied with your map-reduce, use it to solve these problems:

  1. Anagrams:

    Your input consists of a (listof (tv-pair string string)) in which the tag is the file name, and the value is the string contents of the file (like in the word count example). For example, an input tv could be (tv "words.txt" "star rats tarts"). Your output should be a (listof (tv-pair 'm (listof string))) in which each tag represents a unique anagram group, and the value is a list of unique words that appear in the input that are all anagrams of each other. We will define anagrams to be strings made up of the exact same characters (as opposed to letters). Name your functions anagram-map and anagram-reduce.

  2. Nile:

    You and your friends are creating Nile, which you hope will be the next big thing in on-line bookstores. You know that you can save money by anticipating what books people will buy; your brainstorm is that you will pass these savings on to your users by offering a discount if they buy books that Nile recommends.

    To do this, you offer an incentive for people to upload their lists of recommended books. From their lists, you can establish suggested pairs. A pair of books is a suggested pair if both books appear on one person's recommendation list. Of course, some suggested pairs are far more popular than others; and for a given book, it is paired with some books much more frequently than with others.

    You need to organize the list of recommended books to support two tasks:

    • Recommend a book:
      When someone buys a book, you want to be able to suggest a second book to accompany it. Specifically, you should provide the book it is most frequently paired with, along with a count of how frequent this is. Because there may be more than one book with that count, you should return a list of books (even if there is only one) as well as the count. See below for an exact description.
    • Recommend a pair:
      Sometimes, a bored user asks for a book recommendation. Nile offers not one recommendation at a time, but pairs of them! To support this, you must be able to identify the most popular pairs of suggested books. Return the single most popular pair, with a count of how often it occurs. Again, because there may be multiple pairs with the same count, you should return a list of pairs (even if there is only one). See below for an exact description.

    Each tagged file contains a single input list, with single book descriptions on each line. That is, book descriptions will be separated by "\n". Each book has a unique and unambiguous description; that is, if two lines in two different input lists are identical, they refer to the same book. Otherwise they refer to different books. Input lists will always contain at least two books, and they will never contain duplicates. For example, an input tv could be
    (tv "boblist.txt" "Harry Potter\nTwilight\nAnna Karenina").

    You will be implementing the following functions:

    (recommend (title : string)
               (book-records : (listof (tv-pair string string))) : (tv-pair number (listof string)))

    which takes a book title and a list of tagged files (like in Anagrams, except formatted according to the Nile spec) and produces a tv-pair with the highest number of times the book was paired with one or more other books, and a list of those books.

    (popular-pairs (book-records : (listof (tv-pair string string))) : (tv-pair number (listof string)))

    which takes a list of tagged files and produces a tv-pair with the highest number of times any two books were paired with each other and a list of the corresponding pairs. The two books in a pair should be separated by a "+", for example "book1+book2". For your output, each pair of books should only appear once, and order is irrelevant.


You are expected to test anything appropriate with an oracle. The TA staff will be very impressed if you can come up with a generalized oracle for the map reduce framework, however this is not necessary.


What to turn in:

Your analysis must be in a pdf file on Letter-sized paper. Mathematical content must be formatted appropriately. Please, no Arial.