Introduction to Scheme: Syntax, Special Forms, Applicative and Normal Order Evaluation, Study notes of Compilers

An introduction to Scheme, a dialect of Lisp, focusing on its syntax, special forms, and evaluation methods: applicative and normal order. Scheme is a functional programming language that originated from the lambda calculus and is used for academic research, computational models, prototyping, and AI research. It is quite different from imperative languages like Fortran, Algol, Pascal, C, C++, Java, Perl, and Python. In Scheme, expressions are evaluated, and the resulting value is printed. user-defined procedures, conditional expressions, and evaluating procedure applications.

Typology: Study notes

2021/2022

Uploaded on 09/27/2022

thimothy
thimothy 🇬🇧

4

(12)

217 documents

1 / 10

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
CS 450
Lecture 1: Introduction to Scheme—Syntax, Special Forms,
Applicative and Normal Order Evaluation
Carl D. Offner
1 Introduction
Scheme is a dialect of Lisp (“lots of idiotic silly parentheses”). It is the second oldest programming
language, after Fortran. Amazingly enough, however, both are still used:
Fortran: used for scientific applications, number crunching, massive amounts of data. Compilers
for Fortran can generate extremely efficient code.
Lisp: used for academic research into programming language design, computational models, pro-
totyping, AI research, natural language studies, .. . .
Languages such as Fortran, Algol, Pascal, C, C++, Java, Perl, Python are examples of imperative
programming languages.
Lisp has some imperative constructs, but its soul is functional. In particular, it developed originally
from a computing model called the lambda calculus, which was invented in 1936 by Alonzo Church
to help in investigating what functions could be regarded as computable.
Church-Turing Hypothesis: Those functions which are expressible in lambda calculus (which
turn out to be the same as all those functions which are computable by Turing machines) constitute
exactly the class of functions which we naturally regard as computable.
To program in a functional language such as Scheme, you really have to forget everything you
think you know about programming in Java, or C++, or Fortran, and start from the beginning.
As we get deeper into Scheme, we will see how it can model different kinds of languages for us.
2 Some first things to know about Lisp
1. Lisp is typically interpreted rather than compiled.
2. Parentheses always have meaning in Lisp, and it is not the meaning you are used to.
3. Identifiers (e.g., symbols) are lexically more diverse than in most programming languages. For
instance
1
pf3
pf4
pf5
pf8
pf9
pfa

Partial preview of the text

Download Introduction to Scheme: Syntax, Special Forms, Applicative and Normal Order Evaluation and more Study notes Compilers in PDF only on Docsity!

CS 450

Lecture 1: Introduction to Scheme—Syntax, Special Forms,

Applicative and Normal Order Evaluation

Carl D. Offner

1 Introduction

Scheme is a dialect of Lisp (“lots of idiotic silly parentheses”). It is the second oldest programming language, after Fortran. Amazingly enough, however, both are still used:

Fortran: used for scientific applications, number crunching, massive amounts of data. Compilers for Fortran can generate extremely efficient code.

Lisp: used for academic research into programming language design, computational models, pro- totyping, AI research, natural language studies,....

Languages such as Fortran, Algol, Pascal, C, C++, Java, Perl, Python are examples of imperative programming languages.

Lisp has some imperative constructs, but its soul is functional. In particular, it developed originally from a computing model called the lambda calculus, which was invented in 1936 by Alonzo Church to help in investigating what functions could be regarded as computable.

Church-Turing Hypothesis: Those functions which are expressible in lambda calculus (which turn out to be the same as all those functions which are computable by Turing machines) constitute exactly the class of functions which we naturally regard as computable.

To program in a functional language such as Scheme, you really have to forget everything you think you know about programming in Java, or C++, or Fortran, and start from the beginning. As we get deeper into Scheme, we will see how it can model different kinds of languages for us.

2 Some first things to know about Lisp

  1. Lisp is typically interpreted rather than compiled.
  2. Parentheses always have meaning in Lisp, and it is not the meaning you are used to.
  3. Identifiers (e.g., symbols) are lexically more diverse than in most programming languages. For instance

2 3 SOME EXAMPLES

  • *a is a symbol.
    • a is two symbols (* followed by a).
    • is pre-defined to be the addition operator. But it could be redefined to be something else.
  • On the other hand, signed numbers are a special case:

+3 is the number 3 −3 is the number − 3

  1. Lisp is an expression language. In its pure form, there are no imperative statements. Instead, the interpreter

reads an expression, evaluates it, and prints the resulting value (which might be another expression).

That is, the interpreter itself (which is of course a computer program) is implemented as what usually called a read-eval-print loop.

3 Some examples

To bring up the UMB Scheme interpreter, simply type “scheme” at the Unix prompt. The UMB Scheme prompt is ==>.

Here is a more substantive example, illustrating a procedure application (which is what most of the rest of us call a “function call”):

Expression Evaluates to

(+ 7 4) 11

How is this read?

  • The parentheses indicate a function call (in this case; they can also indicate a special form, which we will discuss later).
  • The first element of the list inside the parentheses is the procedure—in this case, it is the procedure indicated by +, which is addition.

4 4 SCHEME SYNTAX

4 Scheme syntax

This may seem very strange to you: There are no statements in Scheme! Everything is an expression. Here are (pretty much) all the different kinds of expressions:

expression

simple expression There are two kinds of simple expressions: constant (e.g., 17 , -6, +2.11, "hello", #t, #f) variable name (e.g., abc, count,... ) compound expression (“compound” in this context just means “not simple”) A com- pound expression is a sequence of expressions (each of which may be a simple or compound expression) enclosed in parentheses: (expr1 expr2 ...). There are two kinds of compound expressions: procedure call (e.g., (+ 5 -11), (square 6)) special form (e.g., (define x 2))

Procedures are either

primitive (i.e., pre-defined in the language, like +), or user-defined (e.g., square)

In some languages, there is a difference between a procedure and a function. In Scheme they mean the same thing. A procedure application is just a fancy way of saying a function call.

4.1 User-defined procedures

User-defined procedures are created by an extension of define:

(define (square x) (* x x))

We know we are defining a procedure and not a variable because a parenthesis comes after the define. This expression is scanned as follows:

(define (square x) (* x x))

name of formal body of procedure parameter procedure

Of course, there can be more than one formal parameter. Also, the body of the procedure can consist of more than one expression. We will see examples of this later. In such a case, all the expressions in the body are evaluated, in the order written. The value of the final one is the value of the procedure application.

This expression can even easily be translated into English, as follows:

(define (square x ) (* x x ))

To square something, multiply it by itself

Now with this definition, we can do the following:

Expression Evaluates to

(square 10) 100 (square (+ 2 5)) 49 (square (square 3)) 81

Now we can make a further definition:

==> (define (sum-of-squares x y) (+ (square x) (square y)))

sum-of-squares ==> (sum-of-squares 3 4)

25 ==>

(Note that we are using hyphens in the middle of symbol names. This is OK in Scheme. It wouldn’t work in C, or Java, or Python, or Perl.)

Let’s go farther:

==> (define (F A) (sum-of-squares (+ A 1)(* A 2)))

f ==>

Exactly how does the Scheme interpreter evaluate something like (F 5)? We’ll get to that shortly. First, lets handle conditional expressions:

5 Conditionals

We have two special symbols for truth values (also called “Booleans”):

#t (means true)

#f (means false)

This is much better than C and languages derived from it, like Java, which really don’t have a good notion of Boolean values.

In Scheme, an expression in a Boolean context is regarded as true if and only if it evaluates to anything except #f.

So we see that a function evaluates all its arguments unconditionally. This may seem trivial or obvious, but it actually isn’t. We’ll be talking a lot more about this as the course goes on.

The reason that if, cond, and define are called special forms is that even though they look like functions (in that they are the first elements of lists inside parentheses), they act differently. For instance, they may not evaluate all their arguments like functions do. And they may not return a value. (The define special form, for instance, does not evaluate both its arguments—it only evaluates the last one—and it also does not return a value.)

Here is an example of applicative-order evaluation:

Recall some of our user-defined procedures:

(define (square x) (* x x)) (define (sum-of-squares x y) (+ (square x) (square y))) (define (F A) (sum-of-squares (+ A 1)(* A 2)))

To evaluate (F 5), we proceed like this:

(F 5) (sum-of-squares (+ 5 1)(* 5 2)) (sum-of-squares 6 10) (+ (square 6) (square 10)) (+ (* 6 6) (* 10 10)) (+ 36 100) 136

7 The lambda special form

Well, what we did was actually not quite correct, because it is not quite what we said we would do. We really should do something like this: Evaluate F and 5 to transform (F 5) into

("function having one parameter -- call it A -- and whose value is (sum-of-squares (+ A 1) (* A 2))" 5)

which then becomes (when we apply the function)

(sum-of-squares (+ 5 1)(* 5 2))

and then we proceed as before.

There is a way of doing this: we use another special form. We could have defined

(define F (lambda (A) (sum-of-squares (+ A 1)(* A 2))))

Thus, (lambda (x) ()) is an unnamed function of 1 parameter whose body is ().

In fact, (define (f x y) ()) is really turned by the interpreter inter- nally into

(define f (lambda (x y) ()))

8 8 SOME EXAMPLES OF THE USE OF LAMBDA

8 Some examples of the use of lambda

==> ((lambda (x) (+ x 3)) 4)

7 ==>

The trouble with this, of course, is that since this function has no name, it can only be used once. So the idea is that we can use define to give it a name:

==> (define f (lambda (x) (+ x 3)) )

f ==> (f 4)

7 ==> (f -3)

0 ==>

and so on.

If we entered this expression ==> (define (f x) (+ x 3))

f ==>

then the interpreter actually turns it internally into the previous one: (define f (lambda (x) (+ x 3)) )

Here is how (F 5) is really evaluated internally by the Scheme interpreter:

(F 5) ;; Now eval F ((lambda (A) (sum-of-squares (+ A 1)(* A 2))) 5) ;; Now apply the function (sum-of-squares (+ 5 1) (* 5 2)) ;; Now eval every element ((lambda (x y) (+ (square x)(square y))) 6 10) ;; Now apply the function (+ (square 6) (square 10)) ;; Now eval every element (+ ((lambda (x) (* x x)) 6) ((lambda (x) (* x x)) 10)) ;; Now apply lambdas (+ (* 6 6) (* 10 10)) ;; Now eval every element (+ 36 100) ;; Now apply the function 136

Some more examples:

10 9 NORMAL-ORDER EVALUATION

Let’s evaluate (F 5) using normal-order evaluation:

(F 5) ;; Now eval F ((lambda (A) (sum-of-squares (+ A 1)(* A 2))) 5) ;; Now apply the function (sum-of-squares (+ 5 1) (* 5 2)) ;; Now eval sum-of-squares ((lambda (x y) (+ (square x)(square y))) (+ 5 1)(* 5 2)) ;; Now apply fn. (+ (square (+ 5 1)) (square (* 5 2))) ;; Now eval arguments (+ ((lambda (x) (* x x)) (+ 5 1)) ((lambda (x) (* x x)) (* 5 2))) (+ (* (+ 5 1) (+ 5 1)) (* (* 5 2) (* 5 2))) (+ (* 6 6) (* 10 10)) (+ 36 100) 136

You can see why normal-order evaluation is also called lazy evaluation or delayed evaluation— we don’t actually evaluate anything until we absolutely need to.

Basic Fact: Whenever applicative-order evaluation yields a result, normal-order evaluation yields the same result. But there are cases when normal-order evaluation is more powerful.

For example:

(define f (lambda (x y) x))

(f 4 (/ 2 0))

The reason that Scheme uses applicative-order evaluation is:

  • It is easier to implement. This is a minor reason.
  • It leads to much more efficient code. This is the main reason.