CS 214 Lab 3: Clojure


Getting Started

As in lab01, begin by creating a clojure folder containing a src folder. Copy the file year_code.clj into your src folder; then use a text editor (e.g., vim) to edit the file year_code.clj. Take a moment to study it, to see how it implements our basic algorithm, and compare it to the other "programs" you've seen.

Defining yearCode()

An Clojure function has the following structure:

   FunctionDef     ::=   (defn identifier [Parameters]
                            Documentation
                            ExpressionList)
   Parameters      ::=    identifier Parameters | Ø
   Documentation   ::=    "Characters" | Ø
   ExpressionList  ::=    Expression ExpressionList | Ø
Note that unlike C-family parameters, Clojure parameters are merely listed within the square brackets following the identifier that names the function; parameters' types are determined at run-time, by the arguments passed (and the operations applied) to them.

From a functional point of view, our yearCode() function has this specification:

Receive: year, a string.
Precondition: year is one of {freshman, sophomore, junior, senior}.
Return: The integer code corresponding to year (1, 2, 3 or 4).

Using this specification and the preceding BNF, define a stub for function yearCode() above the function named -main.

Like the parameter types, a Clojure function's return-type is determined dynamically, at run-time, by whatever value the function returns (which is the final value the function computes). For this reason, the function's return-type is not declared.

Using an if Function

To complete the stub and implement our algorithm, we need the syntax of the Clojure if construct. As we have seen, everything in Clojure is either a function or an argument to a function, so where most other languages have an if statement, Clojure has an if function:

   IfFunction      ::=   (if CondExpr ThenExpr ElseExpr)
   CondExpr        ::=   Expression
   ThenExpr        ::=   Expression
   ElseExpr        ::=   Expression | Ø
When the if function is called, CondExpr is evaluated. If its value is not nil, then ThenExpr is evaluated and its value is the return-value of the if function; otherwise, if ElseExpr is present, that expression is evaluated and its value is returned. The if function thus returns the value of the last Expression it evaluated. LISP-family languages use nil as the boolean value "false" and non-nil as the boolean value "true".

An if-else-if form can thus be built simply by making ElseExpr another if function. The structure of an if-else-if will thus be similar to that of the C-family if, but without any else keyword.

To fill in the CondExpr of each if, we need to know how to compare two strings in Clojure.

   CondExpr        ::=   BoolExpr | Predicate | RelationalExpr | number | ...
   BoolExpr        ::=   (BoolFunction Expression Expression)
   BoolFunction    ::=   and | or
   Predicate       ::=   (UnaryOp Expression)
   UnaryOp         ::=   not | Predicate
   RelationalExpr  ::=   (RelationalOp Expression Expression)
   RelationalOp    ::=   = | not= | < | > | <= | >= | ...
   Predicate       ::=   identical? | integer? | double? | number? | string? | ...

The = operator returns "true" if its arguments are equal, so we can use it here. (The identical? predicate returns "true" if its two arguments are references to the same object.)

Using the preceding information, add the necessary if functions to function yearCode() to implement our algorithm.

To complete the function, we need to know how to return a value from a Clojure function. The LISP-family function-return-value mechanism is quite simple: the return-value of a function is the value of the final expression it executes. That is, if the last function executed by yearCode() is our if function, then yearCode()'s return value is the value returned by the if function (whose return-value is the value of the final Expression it executes). Of course, an Expression can be a simple number:

   Expression      ::=   number
Using this information, add the necessary expressions to the function, so that when it terminates, it will return the appropriate code for a given value of year.

Save your program, compile, and test it, as we did in lab01. Make sure that yearCode() works correctly for all valid and an invalid input before continuing.

Using the cond Function

LISP-family languages also have a function named cond for multi-branch selective execution. This function looks something like the multibranch switch statement in C-family languages, or the case statement in Algol-family languages, but where those statements are limited to comparing integer-based values, the LISP-family cond function can be used to compare arbitrary values: integer-based, real numbers, strings, etc. Since our problem involves comparing string values, the cond function provides an alternate way to solve our problem. Its syntax can be specified as follows:

   CondFunction   ::=    (cond CondClauses ElseClause)
   CondClauses    ::=    CondClause CondClauses | Ø
   CondClause     ::=    BoolExpr Expr
   ElseClause     ::=    :else Expr | Ø
The cond function starts at the top and goes through its CondClauses until it finds one that evaluates to true; it then evaluates and returns the Expr associated with that CondClause, skipping the remaining CondClauses. If an ElseClause is provided and none of the preceding CondClauses were true, then the Expr of that ElseClause is returned; otherwise the cond function returns nil.

To illustrate its use, here is a yearCode2() function definition that solves our problem using the cond function:

;; solution using the cond function
(defn yearCode2 [year]
  (cond
    (= year "freshman")  1
    (= year "sophomore") 2
    (= year "junior")    3
    (= year "senior")    4
    :else                0
  )
)
Note that unlike the switch or case statements of other languages, the LISP-family cond function does not provide any performance-advantage over a multi-branch if function. This is because the cond function:
  1. Accepts clauses consisting of arbitrary boolean expressions on arbitrary types, rather than integer-based expressions; and
  2. Proceeds linearly through its clauses until it finds one whose boolean expression is true.
However, for problems whose solutions require multibranch selective behavior, the cond function can provide a more readable, succinct, and elegant solution than a series of nested if functions, as can be seen by comparing yearCode() and yearCode2().

Take a few minutes to copy-paste this definition into your source file below your yearCode() function. Then tweak your -main() function so that it displays the values returned both yearCode() and yearCode2(). Verify that yearCode2() provides the same behavior as yearCode() for the various input values (both valid and invalid). Continue when both functions are working correctly.

Using the case Function

Unlike other LISP-family languages, Clojure also has a function named case for multi-branch selective execution. This function looks and behaves similarly to the multibranch switch statement in C-family languages, and the case statement in Algol-family languages. However, where those statements are limited to comparing integer-based values, Clojure's case function can be used to compare arbitrary values: integer-based, real numbers, strings, etc. Since our problem involves comparing string values, the case function provides yet another way to solve our problem. Its syntax can be specified as follows:

   CondFunction    ::=    (case MatchExpression Cases DefaultExpr)
   MatchExpression ::=    Expression
   Cases           ::=    Literal Expression Cases | Literal ... Literal Expression Cases | Ø
   DefaultExpr     ::=    Expression | Ø
The case function determines the value of its MatchExpression; then it goes through its Cases until it finds a Literal that matches that value; this Literal can be any supported type-literal. Once it has matched a Literal, the case function then evaluates and returns the Expression associated with that Literal, skipping the remaining Cases and DefaultExpr (if present). If a DefaultExpr is provided and none of the preceding Cases were a match, then the DefaultExpr is evaluated and returned; otherwise the case function throws an IllegalArgument exception.

To illustrate its use, here is a yearCode3() function definition that solves our problem using the cond function:

;; solution using the case function
(defn yearCode3 [year]
  (case year
    "freshman"  1
    "sophomore" 2
    "junior"    3
    "senior"    4
                0 ; default
  )
)
Like the switch or case statements of other languages, Clojure's case function provides a performance-advantage over a multi-branch if function or a LISP cond function. It achieves this performance advantage by using a map (aka dictionary) data structure behind the scenes that maps each Literal to its corresponding Expression. Matching the initial MatchExpression to a given Literal just involves searching the underlying map structure for the value produced by MatchExpression. It follows that the value of each Literal must be known when the function is compiled.

Take a few minutes to copy-paste this definition into your source file below your yearCode2() function. Then tweak your -main() function so that it displays the values returned by yearCode(), yearCode2(), and yearCode3(). Verify that yearCode3() provides the same behavior as yearCode() and yearCode2() for the various input values (both valid and invalid). Continue when all three functions are working correctly.

Wrapping Up

When year_codes.clj works correctly, follow the same steps as before to create a script.clojure file in which you cat your source program and show it running correctly with each valid input and one invalid input. Quit script and then move your script.clojure file from your clojure project folder "up" to its parent folder (03), with the other script-files.

That concludes the Clojure part of this lab.


Calvin > CS > 214 > Labs > 03 > Clojure


This page maintained by Joel Adams.