Home CPSC 401

Racket

 

Overview

Racket is a functional programming language in the Scheme family of languages which descend from LISP. It features:


 

Running Code

  • The Command Line

    The racket command can be used as an interactive shell:

    $ racket
    > (+ 1 2)
    3
    > (displayln "Hello World")
    Hello World
    
    The racket command can also be used to run a Racket program in a file:
    
    $ racket -f hello.rkt
    Hello from a file
    

  • The IDE

    Racket also comes with an IDE called DrRacket. It can be launched with:

    
    drracket
    

    The IDE looks something like this:

    If it asks for a language, just choose "Racket".

    The top portion is a code window where function definitions can be placed. The bottom portion is an interactive shell for calling functions and evaluating expressions.

  • On the CPSC server, racket is installed and can be run as above.

    Racket can also be easily installed on your own computer.


     

    S-Expressions

    Racket is a homoiconic language which means that everything in the language is represented the same way.

    All code and data are represented with S-expressions. An S-expression is defined as:

    1. An atom.
    2. A number of S-expressions inside of parenthesis separated by spaces.

    The use of S-expressions greatly simplify the grammar of Racket. Every language construct is represented the exact same way.

    It also allows for meta-programming where a program can process program code in some way. Because code and data are stored the same way, this is easier than in most languages.


     

    Atoms

    Atoms in Racket are the single entities that can comprise S-expressions. There are several atoms in Racket:


     

    Naming Conventions

    Racket allows more flexibility in naming variables and symbols than most languages. Names can consist of: There are some conventions that are typically used:
     

    Basic I/O

    Printing to the screen is accomplished with the display function. Like all Racket code, a function call is written as an S-expression. We can write hello world as:

    
    ; hello world program
    (display "Hello Word!")
    

    Comments in Racket begin with a semi-colon.

    There is also the displayln function which ends with a new line.

    For input, there is the read function which reads from stdin. It returns the value as if it was entered into the interpreter.

    We can write a program to read in a name and greet it as follows:

    
    (display "Enter your name: ")
    (define name (read))
    (display "Hello ")
    (display name)
    

    This also illustrates the "define" expression which binds a value to a name.

    Racket also has a "printf function. Racket's printf takes "~a" to print a value, and "~n" to print a newline:

    
    (display "Enter your name: ")
    (define name (read))
    (printf "Hello ~a!~n" name)
    

     

    Arithmetic and Functions

    Racket arithmetic is done using prefix function notation:

    
    > (+ 1 2)
    > (+ 1 2 3 4 5)
    > (* 5 (+ 1 2))
    > (/ (* 10 5) (+ 1 1))
    

    Comparisons are done this way as well:

    
    > (< 3 5)
    #t
    > (= 5 4)
    #f
    > (and (<= 3 5) (= 6 6))
    #t
    
    There are a number of arithmetic and logical functions:
     

    Variables

    Racket supports named values in a few different ways. The define function binds a name to a value:

    
    (define x 10)
    

    The name can then be used in following code:

    
    (begin
      (define x 10)
      (displayln x))
    

    More common is let which binds a name only in a specific scope. For example:

    
    (let ((x 5)
          (y 10))
      (+ x y))
    

    The let form takes a list of variable assignments, followed by an expression in which they are used. This code evaluates of course to 15.


     

    If Expressions

    If expressions are given as S-expressions. The first atom is the symbol "if". Next is a test expression, the then expression and lastly the else expression.

    Some examples:

    
    > (if (even? 4) 1 2)
    > (if (< 3 4) (displayln "Yes") (displayln "No"))
    

    There is also a "when" expression which is like the if, but without the else portion:

    
    > (when (< 3 4) (displayln "Yes"))
    

    If the condition is not true, the expression evaluates to nothing. This is what the displayln function returns anyway.

    There is also the "cond" expression which can be used like a switch:

    
    (cond ((< x 0) (displayln "Negative"))
          ((> x 0) (displayln "Positive"))
          ((= x 0) (displayln "Zero")))
    

    The cond form has multiple pairs of tests and results. If the test is "else" it is used as a default case.

    Note that these are all expressions and not statements which allow them to be nested in other expressions:

    
    (define x (if (< y 0) 1 0))
    (displayln (if (< x 0) "Negative" "Positive"))
    

     

    Defining Functions

    As a functional language, Racket programs typically consist of function definitions. A function, like a variable, can be defined with the "define" expression:

    
    (define (name arg1 arg2 ... ) expression)
    

    For example, we can write an add function as:

    
    (define (add a b) (+ a b))
    

    Code does not have to be on one line (Racket does not count whitespace). The add function could also be written as:

    
    (define (add a b)
      (+ a b))
    

    How could we write a function to compute the area of circle

    
    
    
    
    

    How could we write an absolute value function?

    
    
    
    
    

    How could we write a recursive factorial function?

    
    
    
    
    

     

    String and Character Functions

    FunctionMeaning
    (string? s)Returns whether or not s is a string.
    (make-string num char)Creates a string with num charcters set to char.
    (string char1 char2 char3 ...)Creates a string composed of the characters that are passed in.
    (string-length s)Returns the length of the string s.
    (string-ref s i)Returns the character at s[i].
    (substring s start end)Returns the substring of s from start to end.
    (string-append s1 s2 s3 ...)Append all of the arguments together in one string.
    (string=? s1 s2 s3 ...)Test if all arguments are equal.
    (string<? s1 s2 s3 ...)Test if arguments are in increasing order (<=, >, >= exist as well).
    (string-upcase s)Return an upper-case version of s.
    (string-downcase s)Return an lower-case version of s.

     

    Begin Operations

    The S-expressions associated with a function, or if expression must all be single S-expressions. How then can we have multiple lines of code executed in a function?

    The answer is the "begin" expression which is given as:

    
    (begin expr1 expr2 expr3)
    

    begin evaluates all of the expressions it is given, one by one. The value of the begin expression is the value of the last expression in the list.

    Examples:

    
    (begin (display "Hello") (display " there ") (displayln "person"))
    

    begin can be used to insert calls to other functions, such as display, into other expressions. begin is used in the following program to solve the collatz conjecture:

    
    ; solves the collatz conjecture
    
    ; return one step in the sequence
    (define (collatz-step n)
      (if (even? n)
        (/ n 2)
        (+ 1 (* 3 n))))
    
    ; recurse over all numbers
    (define (collatz n)
      (if (= n 1)
        (displayln "All done!")
        (begin
          (display "N = ")
          (displayln n)
          (collatz (collatz-step n)))))
    
    ; get input and run it
    (displayln "Enter a positive number:")
    (collatz (read))
    

    How can we write a guess the number game?

    
    
    
    
    
    
    

     

    Lists

    Lists are the most common data structure in Racket. They are defined as S-expressions:

    
    > (1 2 3)
    

    However, Racket tries to evaluate all S-expressions leading to an error:

    
    application: not a procedure;
     expected a procedure that can be applied to arguments
      given: 1
    

    To tell Racket to not evaluate the list, we use the quote function.

    
    > (quote (1 2 3))
    (1 2 3)
    

    There is a shortcut for this:

    
    > '(1 2 3)
    (1 2 3)
    

    Some common list functions:

    How could we implement the length function using empty? and rest.

    
    
    
    
    
    

    How could we write a function to sum all of the elements in a list?

    
    
    
    
    
    

     

    Lambda Functions

    A lambda function is one that is created without a name, on the fly. This is done with the "lambda" keyword:

    
    (lambda (arg1 arg2 ...) expression)
    

    We can create a function an call it in one S-expression:

    
    ((lambda (x) (* x x)) 5)
    

    This is useful when we want to pass a function to another as we will see below.

    Racket also allows the "λ" character itself to be used in code:

    
    ((λ (x) (* x x)) 5)
    

    It can be entered in Dr Racket with Control-\


     

    Higher Order Functions

    Functional languages make extensive use of higher-order functions. These are functions that take a function as a parameter or return a function as a result.

    A simple higher-order function in Racket is for-each. This function takes two parameters, a function, and a list. It then applies the function to each element of the list. For example, to print a list of strings:

    
    (for-each display '("A" "B" "C" "D" "E\n"))
    

    for-each could be implemented as:

    
    (define (for-each f ls)
      (when (not (empty? ls))
        (begin (f (first ls))
          (for-each f (rest ls)))))
    

    Higher order functions are more commonly used than loops in functional programming.


     

    Folding

    A more complex, though common, operation is to do something with a list keeping track of some accumulation. This is done in the the function we wrote to sum a list, and would be used in finding the product or max of a list. We can extract this out into a function traditionally called fold:

    
    (define (fold f accum ls)
      (if (empty? ls)
        accum
        (fold f (f (first ls) accum) (rest ls))))
    

    This function takes three parameters: a function to apply across the list, the accumulated value, and the list to apply to. It works by checking if the list is empty, if so it returns the accumulated value. Otherwise, it recurses by calling itself with the same function, applying the function to the first item, and passing the rest of the list.

    We can use this to sum a list of integers as follows:

    
    (fold + 0 '(1 2 3 4 5 6 7 8 9 10))
    

    Here we fold "+" across the list with an initial value of 0.

    We can also use it to find factorials:

    
    (fold * 1 '(1 2 3 4 5 6))
    

    Lambda functions go well with higher order functions. For example, to find the max of a list of numbers we can write:

    
    (fold (lambda (a b) (if (> a b) a b)) 0 '(13 5 25 17 8))
    

    The "fold" function is actually included in Racket as "foldl" and "foldr" which work the same, but in different directions. This example demonstrates the difference:

    
    (foldr string-append "" '("A" "B" "C"))
    (foldl string-append "" '("A" "B" "C"))
    

    Imagine we have a list of boolean values. How could we use fold and lambda to determine if all of them are true?

    
    (foldl (λ (a b) (and a b)) #t values)
    
    
    
    If any of them are true?
    
    
    
    
    
    

     

    Mapping

    Another powerful higher-order function is "map". map takes a function and a list, and applies that function to each element returning a new list:

    
    (map (lambda (x) (* x x)) '(1 2 3 4 5 6 7 8 9 10))
    
    How can we use map to compute the lengths of a list of strings all at once?
    
    
    
    
    
    

    How can we implement the map function?

    
    
    
    
    
    

     

    Filtering

    Another powerful function is filter. This is used for selecting only a subset of a list based on some criteria. For example to get all even numbers from a list:

    
    (filter even? '(1 2 3 4 5 6 7 8 9 10))
    

    How could we use filter to get only strings of length at least 3 from a list?

    
    
    
    
    
    

    Filter can also be used to implement the quick sort algorithm:

    
    (define (qsort ls)
      (if (empty? ls)
        '()
        (append (qsort (filter (lambda (x) (< x (first ls))) (rest ls)))
          (list (first ls))
          (qsort (filter (lambda (x) (>= x (first ls))) (rest ls))))))
    

    How can we implement the filter function?

    
    
    
    
    
    

     

    More on Quoting

    Recall that if we want a list of data, we need to quote it:

    
    '(1 2 3 4 5)
    

    When we quote a list, the entire thing is treated as data. Sometimes this is not what we want. For example, if we tried to get a list where each item was calculated:

    
    (define x 10)
    
    '((* x 1) (* x 2) (* x 3) (* x 4) (* x 5))
    
    Then the entire list is treated as data. To have the individual items evaluated, we need to unquote them with the , symbol:
    
    `(,(* x 1) ,(* x 2) ,(* x 3) ,(* x 4) ,(* x 5))
    

    For this to work, we also must use ` as a quote symbol instead of '. This one allows unquoting whereas ' does not.

    This gives us the list (10 20 30 40 50) as expected.

    Another thing that we can do with quoted lists is use them to store code that can be evaluated later. For example, if we have the code:

    
    (define p '(displayln "Hello!"))
    
    Then we essentially are storing a line of code inside of a variable. If we want to evaluate that code at some later point in time, we can do so with the eval function:
    
    (eval p)
    

    This allows a lot of flexibility as code can be stored indefinitely, passed to a function etc.


     

    Macros

    A macro is like a function, except that the parameters are not evaluated before the call. This allows us to define new language constructs.

    Consider the "assert" function in many languages. In C and C++, it works as follows:

    
    #include <iostream>
    
    int main() {
      int x;
      cin >> x;
      assert(x > 0);
    
      // do something with x
      return 0;
    }
    
    When run, the assert will test if x is greater than 0. If so, nothing happens, if not, an error is produced:
    
    a.out: x.cpp:10: int main(): Assertion `x > 0' failed.
    

    Notice that the text of the assertion `x > 0' appears in the message. Below is an attempt to write a assert function in Racket:

    
    (define (assert c)
      (when (not c)
        (display "Assertion `")
        (display c)
        (displayln "' failed.")
        (raise 'AssertionError)))
    

    However, when we run this the message is not as helpful:

    
    > (assert (< 4 3))
    Assertion `#f' failed.
    uncaught exception: 'AssertionError
    > 
    

    It's not possible to write a function that displays the text that is passed to it. For that we need a macro. In Racket, these are introduced with the define-syntax-rule form:

    
    (define-syntax-rule (assert c)
      (when (not c)
        (display "Assertion `")
        (display #'c)
        (displayln "' failed.")
        (raise 'AssertionError)))
    

    Now the parameter "c" is passed in as code, not as a computed value. In the (not c) expression, c is evaluated, however, by using #'c, we get the textual value for display.

    Now our error message is much better:

    
    Assertion `.#<syntax:11:8 (< 4 3)>' failed.
    uncaught exception: 'AssertionError
    

    As another example, we can give Racket a while loop with the following macro:

    
    (define-syntax-rule (while condition body)
      (begin
        (define (loop)
          (when condition
            (begin
              body
              (loop))))
        (loop)))
    

    This macro takes two arguments: the condition and the body. Because it is a macro, both are passed in as code that gets evaluated by the macro itself. This allows the body to be re-evaluated each iteration which is essential.

    It can be used as follows:

    
    (define x 0)
    (while (<= x 10)
      (begin              
        (displayln x)
        (set! x (+ x 1))))
    

    Macros allow for the language to be extended. Many Racket "functions" that we have used are actually written as macros.

    Copyright © 2024 Ian Finlayson | Licensed under a Attribution-NonCommercial 4.0 International License.