CMSC 15100 — Lecture 3

The naming of things

Sooner or later, we will want to keep track of some of our "things" by giving them a name. We can do this by using the define keyword. The general form of this looks like the following


(define name value)
    

By doing so, we bind the name name to the value value. For instance,


(define num-students 60)
    

However, there's one more thing we need to do. We need to explicitly state what type of values can be bound to name. This comes before our definition, like so,


(: name Type)
(define name value)
    

This is called a type ascription or type annotation. This says that we declare that values bound to the name name are guaranteed to have the type Type. So taking our example above, we can have


(: num-students Integer)
(define num-students 60)
    

Functions

Let's think about functions some more. Informally, the broadest definition of a function is just something that maps inputs to outputs. More specifically, every input gets mapped to one particular output. In other words, I can't get two different outputs from the same input. That's it! To get a more formal definition, we can dip into set theory, but you'll find that it's pretty much what we just described. Now, we're very used to the idea of expressions with respect to basic arithmetic, like so

(+ 24 34)

We may not realize it, since we often treat basic arithmetic functions as operations, but $+$ is a function: it maps inputs (two numbers) to an output (another number). We will never have a situation where $24+34$ is ever something that's not $58$.

Boolean expressions

A particularly useful type to think about are Booleans. Functions that produce Boolean values are sometimes called predicates. But it's also perfectly fine to just call things a Boolean ___ (Boolean values, Boolean expressions, Boolean functions, etc.).

Some simple Boolean functions that you will likely have seen before are relations for comparing numbers: $\lt, \gt, =$. We can write expressions in math like $5 \lt 2$, $5 \gt 2$, or $5 = 2$,and these will either be true or false. In this case, $5 \lt 2$ is false, $5 \gt 2$ is true, and $5 = 2$ is false.

Just as before, to turn these into Racket expressions, we simply move the corresponding symbols out front.

(< 5 2)
(> 5 2)
(= 5 2)

Just like with arithmetic operations, it might look strange to write relation symbols using prefix notation, but the idea is exactly the same. Much like with - or /, you will need to be careful for those relations that are not symmetric, > and <.

Now, what if we want to do something more complicated? For example, suppose we wanted to check whether a number was in an acceptable range, like $-5 \lt x \lt 5$. This is really testing two conditions: that $x$ satisfies both $x \gt -5$ and $x \lt 5$.

To express these slightly more complicated conditions, we can use Boolean connectives to join together several Boolean expressions. There are three main boolean connectives: and, or, and not. In Racket, these are conveniently named and, or, and not. As with all the other operations we've seen, these are applied using prefix notation.

Suppose we have two Boolean expressions p and q. The Boolean connectives are defined by

The following table summarizes each of the cases above.

p q (and p q) (or p q) (not p)
#t #t #t #t #f
#t #f #f #t
#f #t #f #t #t
#f #f #f #f

Then our range $-5 \lt x \lt 5$ can be expressed in Racket as

(and (< -5 x) (< x 5))

One other convenience of the and and or functions in particular is that they behave like the arithmetic operations in that they can take a list of arguments rather than just two. Much like the usage in arithmetic, this is very handy when you have a large number of expressions that you want to and or or together.

(and (or (= -5 3) (< 2 5) #f) (not (= 3 4)) #t)

Conditional expressions

We can use Boolean expressions to test conditions and make decisions via conditional expressions. A conditional expression is a special expression that will evaluate based on the Boolean value of an expression. These can be thought of as expressions of the form "If $x_1$ is true, then evaluate to $y_1$; otherwise, if $x_2$ is true, then evaluate to $y_2$; $\dots$; otherwise, evaluate to $z$." As a Racket expression, it looks like the following:


(cond 
  [bool-expr-1 eval-expr-1]
  [bool-expr-2 eval-expr-2]
  ...
  [bool-expr-n eval-expr-n]
  [else eval-expr-z])
    

There are some things to note here. Such a statement is evaluated in order. What this means is that the statement that results from the conditional is the first one whose condition which evaluates to #t. From this, we notice that we have an else as our final statement. This will get evaluated if no other conditions evaluated to #t.

Consider the following example for testing temp against a bunch of temperature ranges in degrees Celsius.


(cond 
  [(> temp 0) "cool"]
  [(> temp 10) "warm"]
  [(> temp 20) "hot"]
  [(> temp 30) "very hot"]
  [else "cold"])
    

Note that in the current arrangement, for any temperature greater than 0, the first condition is satisfied. As a result, none of the other branches will ever get evaluated, except for values below 0. Simply changing the order of the conditions fixes things:


(cond 
  [(> temp 30) "very hot"]
  [(> temp 20) "hot"]
  [(> temp 10) "warm"]
  [(> temp 0) "cool"]
  [else "cold"])
    

One interesting thing here is the use of square brackets, []. Note that this is purely a stylistic convention because it makes it easier to read. One can use regular old parentheses here if so desired.

Defining functions

Once our programs start getting to a certain level of complexity, it becomes difficult to think of programs as very complicated expressions all the time. Just as we did with values, we can give functions names. But in this case, giving functions names does something more interesting than giving values names. This allows us to reuse functions. And since functions are our basic unit of computation, we can think of functions as programs.

This means that we want to put a bit more care in how we define functions. To do this, we'll introduce a process that will help to guide your problem solving process. For every function you write in this class, no matter how big or small, you are required to include the following:

  1. Type ascription
  2. Purpose
  3. Definition
  4. Tests

Each one of these pieces serves a purpose. Let's go through them in turn, with the intent of turning our Celsius temperature discriminator into a reusable function called cel-scale.

Type ascription

Recall that when defining values, we were required to declare its type. We are required to do the same thing with functions. You might ask what the type of a function is. Here, we have to go back to some mathy notation. Recall that in math we define functions in the following way \[f : \mathbb R \times \mathbb R \to \mathbb R\] This says that our function $f$ takes two real numbers and produces a real number. We say that $\mathbb R \times \mathbb R$ is the domain of $f$ and $\mathbb R$ is the co-domain or range is of $f$.

In other words, types for a function will define and enforce the type of the parameters for the function and the type of the result.

To say this kind of thing in Racket, we say

(: f (-> Real Real Real))

This looks a bit strange, until you remember the Lisp-y way of writing things. However, there are some very good non-Lisp motivated reasons for interpreting functions written in this way. Strangly enough, Racket will let you write this in a more familiar way.

(: f : Real Real -> Real)

Here are the general forms for type ascriptions, for a function named function-name with $n$ parameters of the types given in order, and produces a value of type Return-Type


(: function-name (-> Type-1 Type-2 ... Type-n Return-Type))
(: function-name : Type-1 Type-2 ... Type-n -> Return-Type))
    

Which one should you use? It's up to you. Most people gravitate towards the infix version because we're just used to reading things in infix. Personally, I've found the prefix version more handy for some of the more complicated types we'll see this quarter.

Here is the type ascription for cel-scale.

(: cel-scale (-> Real String))

So what is the point of the type ascription? The obvious answer is that Typed Racket demands it. But if we were to look at ordinary Racket, we would realize that we need some way of determining the types of our inputs and outputs anyway. And that is a pretty important step of designing your program, to figure out what kinds of inputs and outputs it is that you actually want to work with.

Purpose

Every programming language has a facility for including notes intended for humans to read that are ignored by computers. These are called comments. In Racket, comments begin with semicolons, ;.


(define cool-value 3193) ; this value is very cool
    

Anything that follows a semicolon is ignored. By convention, we use a single semicolon (like above) for comments that begin after a line of code and two semicolons for comments that begin on a line on their own.

The purpose is a human-readable comment that describes the purpose of the function to the reader. For instance, for cel-scale, we might say something like

;; Produces a string based on the temperature in degrees Celsius.

You might wonder what the point of this is. After all, if you are an incredible programmer, then your program should be so brilliant that it explains itself. This is an easy trap to fall into. The issue is that while the code will explain literally what it does, it is not very helpful for determining whether the code is doing what it was intended to do. In other words, the purpose documents intent. Such documentation is extremely valuable to everyone who will read your code—including yourself in a few weeks.

Definition

This is actually just the body of the function, which consists of the expression(s) you wrote. The real work of this part is solving the problem and writing the code for it. Defining a function is similar to defining a value, with the extra work of naming the parameters in addition to the function:


(define (function-name arg1 arg2 ... argn)
  expr)

Here, expr is the expression that constitutes the body of your function. One of the things that is implicit about this is that a function is just the name for a particular expression and that the value of that expression is the value that the function evaluates to.

We have the definition for cel-scale ready to go:


(define (cel-scale temp)
  (cond 
    [(> temp 30) "very hot"]
    [(> temp 20) "hot"]
    [(> temp 10) "warm"]
    [(> temp 0) "cool"]
    [else "cold"]))
    

Tests

The final piece of function design is providing tests. Why do we need tests? A common question that many beginners have is "How do I know my code is correct?" In fact, this is a tricky problem that even seasoned engineers will struggle with.

As a theoretician, my snide answer would be that it's simple, just provide a mathematical proof! But it turns out most people do not want to do this. The next best thing is to provide tests as evidence that your program works.

Luckily, Racket has built-in testing facilities that make defining tests quite simple. We use the function check-expect to define tests. These have the general form

(check-expect expr1 expr2)

When a special function is called, Racket will run through these tests and determine whether the value of expr1 is equal to the value of expr2 and produce a report. So a test like

(check-expect (cel-scale -2) "cold")

is intended to check whether (cel-scale 2) really produces the value we expect, "cold".

A natural question to ask is how many tests we want. To answer that, we will turn the question back on you: how many tests does it take to really convince yourself that your program really works? This question can guide not only the quantity of tests but what kinds of tests you should be writing.

Putting it all together

Once we have all of this set up, it looks something like this


(: cel-scale (-> Real String))
;; Produces a string based on the temperature in degrees Celsius.
(define (cel-scale temp)
  (cond 
    [(> temp 30) "very hot"]
    [(> temp 20) "hot"]
    [(> temp 10) "warm"]
    [(> temp 0) "cool"]
    [else "cold"]))

(check-expect (cel-scale -2) "cold")
(check-expect (cel-scale 0) "cold")
(check-expect (cel-scale 1) "cool")
(check-expect (cel-scale 45) "very hot")
    

One thing to keep in mind is that the order of the four parts is just the order we want to see them in your code. It does not mean that you should necessarily work on those things in that order.