Grammar Rules of Minimal Scheme

This is an informal treatment of Scheme syntax and grammar. For a more formal treatment, in the standard notation computer scientists use for discussing language syntax, see Syntax and Semantics in the How to Design Programs textbook.

Outline


Calling a function
(function value value ... value)
applies the function to the various values, as appropriate.
Examples:
  • (+ 1 2)
    applies the function "+" to the values 1 and 2; in other words, it adds 1 and 2.
  • (* (+ 3 4) 5)
    applies the function "*" to two values: the first is the result of applying the function "+" to 3 and 4, and the second is the number 5.  It's equivalent to the algebraic expression (3+4)*5.
  • (= x y)
    applies the function "=" to two values: the value of the variable "x" and the value of the variable "y".  It returns true or false, depending on whether those two values are equal.
Common Pitfalls:
  • All operators go before their arguments, not in between as in algebra.  (A benefit of this is that you can apply an operator to more than two arguments, e.g. (+ 1 2 3 4 5) .)
  • Parentheses are required around every operator.
  • "Extra" parentheses are not harmless, as they are in algebra.  The first thing after a left parenthesis is always supposed to be a function or operator.
Incorrect examples:
  • (1 + 2)
    because the operator belongs before its arguments.
    If a function name appears in any position other than just after a left-parenthesis, you're probably doing something wrong.
  • + 1 2
    because parentheses are required (see the above rule).
  • ((+ 1 2))
    because extra parentheses are meaningful.  When Scheme tries to evaluate this, it looks at the inner (+ 1 2) and converts it to 3 easily enough, but then it looks at the outer parentheses, observes that the first thing inside them is 3, and complains that 3 isn't a function.
    If anything other than a function name appears just after a left-parenthesis, you're probably doing something wrong.
Defining a variable
(define variable-name value)
adds a variable to the language, so that subsequent references to variable-name are replaced by value.
Examples:
  • (define bignum 1234567890)
    produces no result at all, but has the side effect of creating a new variable bignum, so that subsequently...
  • bignum
    produces the result 1234567890, and...
  • (* bignum bignum bignum)
    produces the result 1881676371789154860897069000.
Common pitfalls:
  • Remember that a variable name cannot contain spaces. It can contain capital letters, lower-case letters, numbers, hyphens, underscores, and a few other punctuation marks, but it cannot contain spaces.
Defining a function
(define (function-name parameter parameter ... parameter)
        body-expression)
adds a function to the language. Each parameter is a variable name, and normally these variable names are used inside the expression.
Examples:
  • (define (square n) (* n n))
    or, using separate lines and indentation,
    (define (square n)
            (* n n))
    
    produces no result at all, but has the side effect of defining a new function named square, taking one parameter (which it refers to internally as n).  Subsequently,...
  • (square 5)
    returns the value 25.  Scheme matches this with the pattern "(square n)", decides that the parameter n is supposed to be 5 for now, replaces all the "n"'s in the function body with 5, producing "(* 5 5)", and further evaluates this to get 25.
Common Pitfalls:
  • Note that a function name, like a variable name, cannot contain spaces.
  • The parameters in the header of a function definition can be any variable names, and you can use these names again in the expression. However, these names are only meaningful inside the function definition; as soon as the function definition is over, they are undefined. For example, in the above definition of the "square" function, the parameter name "n" is introduced in the header and used in the body, but then it disappears; if you were then to type
    (square n)
    Scheme would complain that "n" was undefined. The only way to use the "square" function is to provide a number (or an expression whose value is a number) to match the "n" in the pattern, e.g. (square 5).
Incorrect examples:
  • (square n)
    because n isn't defined outside the function definition; see above.
  • (square 3 4)
    because the square function only takes one parameter.
  • (square)
    ditto (we just called it with no parameters).
  • (define (square) (* n n))
    because the body of the function uses a variable n which was not defined in the header of the function.
  • (define (square n))
    because the function has no body.
  • (define (square n) (* n n)
    because the parentheses aren't matched properly.
  • (define (square n) (+ n n))
    Scheme won't give you an error message on this, because it's perfectly legal: all the parentheses are matched properly, everything has the right number of parameters, etc. It is, however, wrong, because it produces answers that don't match what the programmer presumably intended. For example, if you typed (square 3) you would get the answer 6 rather than the correct 9. This illustrates the difference between a program being legal and being correct.
Conditionals
(cond [question1 answer1]
      [question2 answer2]
      ...
      [questionn answern])
returns the value of one of answer1, answer2, ... answern, depending on which question is the first to have the value true. If question1 evaluates to true, it returns answer1; if not, it ignores answer1 and continues with the rest of the list of question-answer pairs.

The keyword "else" may appear in place of questionn to indicate that the cond should return the value of answern if no previous question was true.

Another way to look at it is that

(cond [true answer1]
      [question2 answer2]
      ...
      [questionn answern])
simplifies to answer1, while
(cond [false answer1]
      [question2 answer2]
      ...
      [questionn answern])
simplifies to
(cond [question2 answer2]
      ...
      [questionn answern])
Examples:
  • (cond [(> grade 90) 'A]
          [(> grade 80) 'B]
          [(> grade 70) 'C]
          [(> grade 60) (warn 'D)]
          [else (warn 'F)])
    
Common pitfalls:
  • Each clause of the conditional must be enclosed in square brackets (Technically, parentheses work too, but I recommend using square brackets for cond-clauses and parentheses for function calls and function definitions, to keep them straight in your mind).
  • Each clause of the conditional must contain exactly two expressions: a question and an answer.  (This requirement is relaxed in more advanced language levels.)
Incorrect examples:
  • (cond (< bignum 4) 5
          (>= bignum 4) 6)
    
    because the question and answer in each clause must be surrounded by square brackets; this person probably meant
    (cond [(< bignum 4) 5]
          [(>= bignum 4) 6])
    
  • (cond [(+ bignum 4) 5]
          [(- bignum 4) 6])
    


    because the question must return a boolean (the expressions "(+ bignum 4)" and "(- bignum 4)" both return numbers instead)

  • (cond [(< bignum 4) 5)
          ((>= bignum 4) 6]
    


    because the parentheses and brackets aren't matched. The parentheses in (< bignum 4) match nicely, but outside this are a left-bracket and a right-parenthesis, which don't match. Similarly, (>= bignum 4) is matched, but outside it are a left-parenthesis and a right-bracket, which don't match. Finally, there is no right-parenthesis to match the one before cond.

Defining structures
(define-struct structure-name (field1 field2 ... fieldn))
adds several new functions to the language: Write down contracts for all of these as soon as you write a define-struct.
Examples:
  • ; A student is a symbol (name), a number (GPA),
    ;   a symbol (class, e.g.  'freshman, 'sophomore, etc.),
    ;   and a number (ID).
    (define-struct student (name GPA class ID))
    ; make-student: symbol number symbol number => student
    ; student?: object => boolean
    ; student-name: student => symbol
    ; student-GPA: student => number
    ; student-class: student => symbol
    ; student-ID: student => number
    
    The one line of actual code in this example tells Scheme to add a new data structure to the language, along with its constructor (make-student), discriminator (student?), and slot-access functions (student-name, student-GPA, student-class, and student-ID). Subsequently,...
    (make-student 'Joe-Schmoe 2.6 'freshman 123456789)
    
    creates and returns a new "student" object with the specified properties.
    (define my-student (make-student 'Jane-Doe 3.3 'sophomore 987654321))
    
    creates a new "student" object and stores it under the new name "my-student".
    (student-name my-student)
    
    returns 'Jane-Doe
  • (student? 'bluebird)
    returns false
  • (student? my-student)
    returns true
  • (student? (make-student 'Bill 2.8 'junior 918273645))
    returns true
  • (student-GPA my-student)
    returns 3.3
Common pitfalls:
  • You do not need to define make-student, student?, student-name, etc. explicitly; they are defined automatically when you say (define-struct student (name GPA class ID)).
  • The list of names of slots (aka fields) must be in parentheses.