CSC 151-02, Fall 2006 : Schedule : Reading 7
Summary: We explore why and how you might define your own procedures in Scheme.
Contents:
frac Oddities
In previous labs, you've seen that it's possible to define complex
expressions from simpler expressions. For example, we might write the
following for express
.
val
explicitly as a rational number
(/ (inexact->exact (numerator val))
(inexact->exact (denominator val)))
What happens when we want to try this expression using different values? One possibility is to redefine val and then re-execute the expression.
> (define val (sqrt 2))
> (/ (inexact->exact (numerator val))
(inexact->exact (denominator val)))
1 1865452045155277/4503599627370496
> (define val 5)
> (/ (inexact->exact (numerator val))
(inexact->exact (denominator val)))
5
> (define val (exp 1))
> (/ (inexact->exact (numerator val))
(inexact->exact (denominator val)))
2 1617426887497577/2251799813685248
But this seems cumbersome. It would be nice to simply give a name to this expression and use that name. Unfortunately, our current way of naming doesn't quite work.
> (define val 0.75)
> (define frac (/ (inexact->exact (numerator val))
(inexact->exact (denominator val))))
> frac
3/4
> (define val 5)
> frac
3/4
What's going on? Scheme evaluated the expression that accompanies
frac once, when it was
first defined. Hence,
since the expression had a particular value once, it retains that
value forever.
What we'd really like to do is to say that
. You know that Scheme has procedures,
since
you've used lots of built-in procedures, including frac
is a procedure that takes a value as an input and
returns
an appropriate fractionsqrt,
*, cons, and list.
But can you
define your own procedures? Certainly.
You use define to give names to procedures,
just as you use it
to give names for values. The values just look different. The general
form of a procedure is
(lambda (formal-parameters)
expression)
frac
Procedure
For example, we might write our frac
procedure as
(define frac
(lambda (val)
(/ (inexact->exact (numerator val))
(inexact->exact (denominator val)))))
Our frac procedure can now be called as if it
were a built-in
procedure.
> (frac (sqrt 2))
1 1865452045155277/4503599627370496
> (frac 5)
5
> (frac 22/7)
3 1/7
> (frac 1.5)
1 1/2
> (frac 1.2)
1 900719925474099/4503599627370496
> (frac 0.75)
3/4
See the notes at the end of this document for explanation of the stranger results.
We can define procedures for anything we already know how to do in
Scheme.
For example, here is a simple square
procedure.
(define square
(lambda (n)
(* n n)))
We can test it.
> (square 2)
4
> (square -4)
16
> (square square)
*: expects type <number> as 1st argument, given: #<procedure:square>; other arguments were: #<procedure:square>
> (square 'a)
*: expects type <number> as 1st argument, given: a; other arguments were: a
Convention in Scheme (and all programming languages) is that we carefully document what our procedures do, including input values, output values, and assumptions. We use comments provide information to the reader of our program (that is, to people instead of the computer). In Scheme, comments begin with a semicolon and end with the end of the line.
;;; Samuel A. Rebelsky
;;; Department of Mathematics and Computer Science
;;; Grinnell College
;;; rebelsky@cs.grinnell.edu
;;; Procedure:
;;; square
;;; Parameters:
;;; val, a number
;;; Purpose:
;;; Compute val*val
;;; Produces:
;;; result, a number
;;; Preconditions:
;;; val must be a number
;;; Postconditions:
;;; result is the same "type" of number as val (e.g., if
;;; val is an integer, so is result; if val is exact,
;;; so is result).
;;; Citations:
;;; Based on code created by John David Stone dated March 17, 2000
;;; and contained in the Web page
;;; http://www.math.grin.edu/~stone/courses/scheme/procedure-definitions.xhtml
;;; Changes to
;;; Parameter names
;;; Formatting
;;; Comments
(define square
(lambda (value)
(* value value)))
Yes, that's a lot of documentation for very little code. However, it is better to err on the side of too much documentation than too little documentation. More importantly, as you start writing more procedures, their purpose and details will be much less obvious. Finally, when you carefully document procedures, you begin to think more carefully about what they really need to do and how you ensure that they do so for all cases.
Here's another set of documentation, this time for the frac
procedure that we wrote earlier.
When documenting frac, we are forced to think
about (1) what
kinds of numbers it works on (in this case, it doesn't work on complex
numbers); (2) what, precisely, the relationship of the result to the
input is; and (3) what type the result has.
;;; Procedure:
;;; frac
;;; Parameters:
;;; val, a number
;;; Purpose:
;;; Express val as a fraction.
;;; Produces:
;;; rat, a rational number.
;;; Preconditions:
;;; val cannot be complex.
;;; Postconditions:
;;; rat is exact.
;;; rat is approximately equal to val (within some unknown level
;;; of accuracy).
;;; rat is the ratio of two integers.
(define frac
(lambda (val)
(/ (inexact->exact (numerator val))
(inexact->exact (denominator val)))))
At times, we will want to write procedures that take more than one parameter. Such procedures look just like procedures with one parameter, except that you can list more parameters between the parentheses.
(lambda (param1, param2 ... paramn)
expression
)
For example, here is a simple procedure that finds the average of two numbers.
;;; Samuel A. Rebelsky
;;; Department of Mathematics and Computer Science
;;; Grinnell College
;;; rebelsky@cs.grinnell.edu
;;; Procedure:
;;; pairave
;;; Parameters:
;;; val1, an exact number
;;; val2, an exact number
;;; Purpose:
;;; Compute the average of two numbers.
;;; Produces:
;;; ave, The average of those two numbers.
;;; Preconditions:
;;; Both val1 and val2 are exact numbers.
;;; Postconditions:
;;; ave is an exact number.
;;; ave is equidistant from val1 and val2. That is:
;;; (abs (- val1 ave)) equals (abs (- val2 ave))
(define pairave
(lambda (val1 val2)
(/ (+ val1 val2) 2)))
As this example may suggest, in your documentation it is particularly important to think about what you can guarantee about the results of your procedure. In this case, what does it mean to be the average of two values?
frac
Oddities
You may have observed some odd behavior with some of the calls to
frac, such as
> (frac 22/7)
22/7
> (frac 1.5)
3/2
> (frac 1.2)
5404319552844595/4503599627370496
First, you may notice different output if you try some of these
examples on
your computer. For example, some versions of Scheme give results like
22/7. and others separate the whole part and
the fractional
part and write them as something like 3 1/7.
But that's nothing compared to that last answer. What's going on with that last example? It turns out that our implementation of DrScheme currently represents the decimal part of many inexact numbers as a ratio of some number and and 4503599627370496, which happens to be 252. (Most computers like powers of 2.) That's why it's inexact. There's no number, n, such that n/4503599627370496 is 0.2. However, 0.5 is 2251799813685248/4503599627370496, which can be simplified to 1/2.
Janet Davis (davisjan@cs.grinnell.edu)
Created September 3, 2006 based on http://www.cs.grinnell.edu/~rebelsky/Courses/CS151/2006F/Readings/procedures.html