Introduction to Functions
1 Repeated Computations:   Flags Example
2 Defining Your Own Functions
3 Cleaning Up Flag Code
4 More Functions Practice:   Cost of pens

Introduction to Functions

Copyright (c) 2017 Kathi Fisler

This material goes with From Repeated Expressions to Functions from the textbook

1 Repeated Computations: Flags Example

Consider programs to draw the flags of Armenia and Austria. These two countries have the same flag, just with different colors:

  armenia =

    frame(

      above(rectangle(120, 30, "solid", "red"),

        above(rectangle(120, 30, "solid", "blue"),

          rectangle(120, 30, "solid", "orange"))))

  

  austria =

    frame(

      above(rectangle(120, 30, "solid", "red"),

        above(rectangle(120, 30, "solid", "white"),

          rectangle(120, 30, "solid", "red"))))

Rather than write this code twice, it would be nice to write the common code only once, then just change the colors to generate each flag. With what we’ve learned so far, we might do this as follows:

  top = "red"

  middle = "blue"

  bot = "orange"

  

  flag=

    frame(

      above(rectangle(120, 30, "solid", top),

        above(rectangle(120, 30, "solid", middle),

          rectangle(120, 30, "solid", bot))))

This is unsatisfying, however – what if we were trying to create an image that included both flags at the same time? We can only put one defintion of each color in the Pyret dictionary, so there is no way to change the colors to generate another flag within the same run of the program.

Wouldn’t it be nice to be able to do something similar for the flags? It would be nice to just write the following:

  armenia = three-stripe-flag("red", "blue", "orange")

  austria = three-stripe-flag("red", "white", "red")

This is precisely what you are about to learn how to do. You are going to learn how to add your own operators to Pyret.

2 Defining Your Own Functions

(See the textbook or lecture capture for details on what a function is – what follows is just summarizing the code from class)

Here is

  fun three-stripe-flag(top, middle, bot):

    doc: "produce image of flag with three equal-sized horizontal stripes"

    frame(

      above(rectangle(120, 30, "solid", top),

        above(rectangle(120, 30, "solid", middle),

          rectangle(120, 30, "solid", bot))))

  end

With this, the two desired flag expressions work fine.

What if we make a mistake, and try to use the function as follows:

  three-stripe-flag(50, "blue", "red")

What do you think should happen? 50 is not a string (much less a string naming a color). Pyret will give an error on one of the rectangle commands (we’ll see which one in particular in a little while). The point is that the error comes up inside the computation that creates the flag.

It would be much more helpful if we got the error when we tried to use three-stripe-flag. We can annotate the inputs with types telling Pyret what kind of data the function expects to receive:

  fun three-stripe-flag(top-color :: String, mid-color :: String, bot-color :: String):

    doc: "produce image of flag with three equal-sized horizontal stripes"

    frame(

      above(rectangle(120, 30, "solid", top-color),

        above(rectangle(120, 30, "solid", mid-color),

          rectangle(120, 30, "solid", bot-color))))

  end

If you try the erroneous call again, you’ll see that the location where the error is reported has changed.

3 Cleaning Up Flag Code

We started again with the function from last class to make three-stripe flags.

  fun three-stripe-flag(top :: String, middle :: String, bot :: String):

    doc: "produce image of flag with three equal height horizontal stripes"

    frame(

      above(rectangle(120, 30, "solid", top),

        above(rectangle(120, 30, "solid", middle),

          rectangle(120, 30, "solid", bot))))

  end

  

  armenia = three-stripe-flag("red", "blue", "orange")

  austria = three-stripe-flag("red", "white", "red")

We observed that someone might want to make flags of different sizes, but that flags also have a height/width ratio that should be preserved. This suggests taking one dimension (we chose width) as an input and computing the other:

  fun three-stripe-flag(top :: String, middle :: String, bot :: String,

        width :: Number):

    doc: "produce image of flag with three equal height horizontal stripes"

    frame(

      above(rectangle(width, width * 0.25, "solid", top),

        above(rectangle(width, width * 0.25, "solid", middle),

          rectangle(width, width * 0.25, "solid", bot))))

  end

  

  armenia = three-stripe-flag("red", "blue", "orange", 120)

  austria = three-stripe-flag("red", "white", "red", 120)

We talked about how this code uses the same ratio three times, so it should really be a constant:

  RATIO = 0.25

  

  fun three-stripe-flag(top :: String, middle :: String, bot :: String,

        width :: Number):

    doc: "produce image of flag with three equal height horizontal stripes"

    frame(

      above(rectangle(width, width * RATIO, "solid", top),

        above(rectangle(width, width * RATIO, "solid", middle),

          rectangle(width, width * RATIO, "solid", bot))))

  end

  

  armenia = three-stripe-flag("red", "blue", "orange", 120)

  austria = three-stripe-flag("red", "white", "red", 120)

Then we talked about two other modifications: we might want to do the width * RATIO computation only once, storing the result in a variable:

  RATIO = 0.25

  

  fun three-stripe-flag(top :: String, middle :: String, bot :: String,

        width :: Number):

    doc: "produce image of flag with three equal height horizontal

        stripes"

    height = width * RATIO

  

    frame(

      above(rectangle(width, height, "solid", top),

        above(rectangle(width, height, "solid", middle),

          rectangle(width, height, "solid", bot))))

  end

Alternatively, we could create a helper function to do the height computation:

  RATIO = 0.25

  

  fun compute-height(width :: Number):

    doc: "Compute height from width based on ratio

    width * RATIO

  end

  

  fun three-stripe-flag(top :: String, middle :: String, bot :: String,

        width :: Number):

    doc: "produce image of flag with three equal height horizontal

        stripes"

    frame(

      above(rectangle(width, compute-height(width), "solid", top),

        above(rectangle(width, compute-height(width), "solid", middle),

          rectangle(width, compute-height(width), "solid", bot))))

  end

The last version illustrates that you can create additional functions as a way to name repeated intermediate computations. These functions are typically called helper functions. In this specific case, just naming the height computation is sufficient. Sometimes, however, a helper function is useful for letting you name more complex intermediate computations.

4 More Functions Practice: Cost of pens

Imagine that you are trying to compute the total cost of an order of pens with slogans (or messages) printed on them. Each pen costs 25 cents plus an additional 2 cents per character in the message (count spaces as characters for now).

First, let’s write two expressions that do this computation, just to make sure we understand how it should work:

  # if ordering 3 pens that say "wow"

  3 * (0.25 + (string-length("wow") * 0.02))

  

  # if ordering 10 pens that say "smile"

  10 * (0.25 + (string-length("smile") * 0.02))

Having a clear idea of the computation you want to do makes it much easier to write a function to do it. How do we figure out a function that would correspond to these two expressions? We find the places where the two functions are different and replace each spot of difference with a unique descriptive name. These names become the parameters of the function.

  fun pen-cost(num-pens :: Number, message :: String):

    num-pens * (0.25 + (string-length(message) * 0.02))

The initial expressions we developed give rise to our examples. When you write the expected results of the examples, you can either write the final answer or the expression that computed it (each can be useful in certain cases). Here’s the final function:

  fun pen-cost(num-pens :: Number, message :: String):

    doc: "total cost for pens, each 25 cents plus 2 cents per message character"

    num-pens * (0.25 + (string-length(message) * 0.02))

    where:

      pen-cost(3, "wow") is 0.87

      pen-cost(10, "smile") is 10 * (0.25 + (string-length("smile") * 0.02))

What are other good cases to check here? Ordering 0 pens and pens with empty messages are both good cases to check. You don’t have to check negative numbers of pens or messages that aren’t strings though – those fall outside the expected "contract" of the function (we’ll discuss how to properly handle the negatives later).

Once you are comfortable with the idea of writing functions, you would skip this circle-and-label step, and just jump directly to writing the expression with the parameters in place. Use the circle-and-label technique as needed to get yourself to that point.