Lab 6: Understanding Recursion

While LOLing on AOL Instant Messenger, you receive an urgent message from Ashley. She wants you to learn recursion with her!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


In lecture, we have started to write recursive functions, where a function body calls the same function, just on a smaller part of the data. This lab gives you practice with understanding how this works and writing recursive functions of your own. In the first part of lab, you'll draw function diagrams on paper to visualize recursive function calls. Recursion can be a tricky concept to understand at first, so don't forget to call a TA over if you get confused or need clarification. Good luck!

Part 1: Creating Call-Record Chains

Exercise 1: Binning Numbers by their Signs

In data analysis, we often take a column of data and convert it into a small number of fixed "bins": this lets us cluster data for analysis. For example, we refer to December, January, and February as "winter", while March, April, and May are referred to as "spring". The following function bins numbers based on their signs (positive, negative, and zero):

fun signs(numlist :: List<Number>) -> List<String>:
  cases (List) numlist:
    | empty => empty
    | link(fst, rst) =>
      sign = 
        if fst > 0: "pos"
        else if fst == 0: "zero"
        else: "neg"
        end
      # eval point A
      link(sign, signs(rst))
  end
where:
  signs([list: 3, -1, 6, 0]) is [list: "pos", "neg", "pos", "zero"]
end

Task: On paper, draw a nested-box diagram (just like in the presentation check slides 9-10 for an example with the sum function from lecture) showing the unfinished function calls that exist at the point when signs is called with the empty list.

Take a picture of your diagram we'll ask you to upload it after the next problem.

Task: When people first see recursion, they often believe that the function will return the answer for the empty case (since in the code, the function returns a simple value in that case). What aspect of your nested-box diagram illustrates why this doesn't happen?


CHECKPOINT: Call over a TA once you reach this point.


Exercise 2: Creating Palindromes

A palindrome is a word whose characters are in the same order whether read from left to right or right to left. "abba" and "tacocat" are both palindromes.

The following code makes a list of letters into a palindrome:

fun make-palindrome(letters :: List<String>) -> String:
  cases (List) letters:
    | empty => ""
    | link(fst, rst) =>
      # here, + is being used to append strings
      fst + make-palindrome(rst) + fst
  end
where:
  make-palindrome([list: "a", "b", "c"]) is "abccba"
end

Task: Draw the nested-box diagram for the example in the where block up to the call of make-palindrome on the empty list.

Task: Upload the picture of your diagram from the signs problem and a picture of the diagram from this problem to this Google Form. We're not collecting names we just want to see what sorts of pictures you are drawing.

Exercise 3: Counting Emoticons

Impressed by your recursion skills, the engineers of AOL Instant Messenger reached out to you asking for your help on some research. They want to study the frequency of emoticons used within their system. The importer has created a function is-emoticon that checks whether a text is a valid emoticon. The count-emoticon function uses is-emoticon to tally up the number of emoticons used in their service.

fun is-emoticon(msg :: String) -> Boolean:
  (msg == ":)") or 
  (msg == "XD") or 
  (msg == ":'(") or 
  (msg == ":P")
end

fun count-emoticon(msgs :: List<String>) -> Number:
  cases (List) msgs:
    | empty => 0
    | link(fst, rst) =>
      if is-emoticon(fst):
        1 + count-emoticon(rst) 
      else:
        count-emoticon(rst)
      end
  end
end

Consider the following example:

count-emoticon([list: "XD", "LOL", ":P"])

Task: Draw the nested-box diagram for this example of the unfinished function calls that exist when count-emoticon is called on the empty list.

Task: Separately, draw the diagram of unfinished calls that exist at the point when is-emoticon is called with the second element in the list, "LOL". Hint: Remember that the unfinished calls are the ones made on list elements that precede a given element, so this diagram should have less boxes than the first one.

Task: Why don't calls to is-emoticon appear in your box diagram? How is it used within count-emoticon? Write down your answer.

Task: Compare the sequence of unfinished calls in your first diagram for this problem to the unfinished calls for the signs problem. What do you notice is the same and different between the two series of calls? Write down your answer.

Task: Now, think about whether each of the functions you worked on involved mapping or filtering. What hypothesis can you make about the sequence of "local context" terms for map-like functions versus filter-like functions? Write down your answer.


CHECKPOINT: Call over a TA once you reach this point.


Part 2: Recursive Programming Practice

If you have been comfortable writing recursive programs in class, you can skip ahead to Part 3

Imagine that you are playing a game that asks you to generate a collection of words from a specified set of letters. The following functions check various conditions against those words in order to score the game.

For each function that you implement write sequential where-examples (as seen in class).

fun sum(numlist :: List<Number>) -> Number:
  cases (List) numlist:
    | empty => 0
    | link(fst, rst) => fst + sum(rst)
  end
where:
    sum([list: 4, 7, 2]) is 4 + sum([list: 7, 2])
    sum([list: 7, 2]) is 7 + sum([list: 2])
    sum([list: 2]) is 2 + sum([list: ])
    sum([list: ]) is 0
end

Task: Develop a function all-have-t :: List<String> -> Boolean that takes a list of words and determines whether every word in the list contains the letter "t".

Task: Develop a function compute-score1 :: List<String> -> Number that takes a list of words and produces a score which is the sum of the lengths of all the words.

Task: Develop another scoring function compute-score2 :: List<String> -> Number in which the score for a single word is 1 if it has four or fewer characters. For words longer than four characters, its score is the length of the word.

Task: Look at the two functions you've just written. Is there a common pattern to writing "scoring"-style functions? At what point do they differ?

Task: If you aren't sure how these functions are working, draw out call record chains (either with the tool or with the same components on paper) to trace out how the computation is evolving.

Part 3: Richer Recursion Practice

Task: Write a function make-circles(rad-list :: List<Number>) -> Image that takes in a list of numbers representing radii and produces an image consisting of adjacent green circles. Each circle should have the radius of its corresponding list index. In other words, the first circle should have the same radius as the first element of rad-list, and so on. The final circle in the image should always be a black circle of radius 10, which is not represented in the list.

For example, make-circles([list: 40,60,10]) should produce an image that looks like the following:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Task: Now, we want a function that lets us change the colors of the circles as well. The function make-better-circles(rad-list :: List<Number>, color-list :: List<String>) -> Image should take in:

  • A list of Numbers representing radii
  • A list of Strings representing valid Pyret colors

As before, the function should output an image of adjacent circles. This time, each circle should have both the color and radius of its corresponding list index. The first circle should be the same color as the first element of color-list and the size of the first element of rad-list, and so on. The final circle in the image should still be a black circle of radius 10, not in either list.

For example, make-better-circles([list: 30,40,50], [list: "orange", "olive", "blue"]) should produce an image that looks like the following:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Task: Write out a sequence of related examples to show how the two lists interact within and across the examples.

Task: Write the code for this problem. (Note: Part of the point of this problem is to have you think through which of the two input lists is "driving" generation of the circles. That should be the list you break down in the cases expression.)

Task: Modify your make-better-circles function to raise an error if there aren't enough colors in the color list for all of the radii

Having trouble dealing with 2 lists?:

Apart from using cases statements, we can access a list's first and rest field by using .first and .rest
For example, if a list is named "my-list", we can access the first item in the list by typing
my-list.first


CHECKPOINT: Call over a TA once you reach this point.


More Challenging Task: Write a new version of make-better-circles called make-circles-recycle in which the list of colors can be shorter than the list of radii. The circles should cycle through the list of colors in order, repeating until all circles have been generated. For example, given a list of four radii and the two colors [list: "red", "brown"], the circles would appear in colors red, brown, red, brown.

Warning: it is easy to make this unnecessarily complicated, especially if you have programmed before. This is an exercise in elegance. If you find yourself thinking about this by counting and modulo, for example, try again!


Optional: Lab Partner Feedback Form