On this page:
4.1 Organizing your code to keep your graders sane
7.5.0.17

4 Organizing Racket Code

Functional languages, such as Racket, often give you more freedom in organizing your code base than you might be used to, but this freedom can make it hard to know how to keep your code base orderly. Languages like Java force a correspondence between classes and files, almost forcing you to organize your code. Racket doesn’t care about how functions are organized into files. While you can put all your code in a single file, this makes browsing the code hard, complicates version control and merge conflicts, defeats separate compilation, and just feels gross.

Typically, a Racket program organizes groups of functions that are conceptually related into a file. We might gather all functions related to register allocation into a single file. But maybe that file is too big (a subjective experience, not a clear dividing line), so we divide further: all functions related to conflict analysis in one file, all functions related to undead analysis in another. Often, we have a few functions that are needed in various placed, but don’t fit in any conceptual group. It’s common to stick these functions in a miscellaneous file, perhaps named utils.rkt.

We can emulate anything we would do with files by using module. The below examples demonstrate organizing code into separate modules, which we could store in separate files.

Examples:
; Behaves like a file named "utils.rkt" that begins "#lang racket"
> (module utils.rkt racket
    ; (all-defined-out) tells the module system to export everything
    ; defined at the top-level.
    (provide (all-defined-out))
    (define (binop? o)
      (and (member o '(* +)) #t))
    (define (aloc? o)
      (and (symbol? o)
           (regexp-match-exact? #rx".+\\.[0-9]+" (symbol->string o))))
    (define (triv? o)
      (and (aloc? o)
           (triv? o))))
; Behaves like a file named "checker.rkt" that begins "#lang racket"
> (module checker.rkt racket
    (require 'utils.rkt)
    (provide check-ae-lang)
  
    (define (check-ae-lang ls)
      (define (check-statement s)
        (match s
          [`(set! ,aloc ,int)
           #:when (and
                   (aloc? aloc)
                   (integer? int))
           s]
          [`(set! ,aloc1 (,binop ,aloc2 ,triv))
           #:when (and
                   (aloc? aloc1)
                   (aloc? aloc2)
                   (binop? binop)
                   (triv? triv))
           s]
          [_ (error "Unexpected statement" s)]))
      (match ls
        [`(begin ,s ...)
         `(begin ,@(map check-statement s))])))
> (require 'checker.rkt)
> (check-ae-lang '(begin (set! x.1 5)))

'(begin (set! x.1 5))

Tests are not commonly placed in-line with code, the way you were taught in 110, but split out in to separate files. The Racket testing framework can automatically scan all files for (module+ test ...) and run the test suites it finds. It’s common to split tests into conceptually related files, too. For example, I might group all tests related to register allocation into one file, and tests that check the interpreter against the compiler into another.

Examples:
> (module test-checker racket
    (require 'checker.rkt)
    (module+ test
      (require rackunit)
      (check-equal?
       (check-ae-lang '(begin (set! x.1 5)))
       '(begin (set! x 5)))
      (check-exn
       (thunk (check-ae-lang '(begin (+ x.1 5)))))))
> (module test-utils racket
    (require 'utils.rkt)
    (module+ test
      (require rackunit)
      (check-true (triv? 5))
      (check-false (triv? `(set! x.1 5)))))

It’s common to put all the test files in a separate test directory.

Of course, your assignments dictates that there be a particular file that exports particular names; thankfully, Racket’s module system allows reproviding definitions you’ve imported from elsewhere. For example, Assignment 3 requires there be a file called a3.rkt in which you define certain functions.

This requirement isn’t about where things live, but about an interface. I need to be able to import that file and get access to certain functions. One way to implement that interface is to literally define the functions in the file a3.rkt. Another way is to use all-from-out to reprovide functions that were imported from elsewhere. This could allow a3.rkt to be completely empty except for require and provide declarations; this is a common pattern in Racket.

Examples:
; Racket views files as modules.
> (module a3.rkt racket
    (require 'checker.rkt 'utils.rkt)
    (provide (all-from-out 'checker.rkt)))
> (require 'a3.rkt)
; I have access to `check-ae-lang`, which is defined in
; "checker.rkt", despite only importing the module "a3.rkt"
> (check-ae-lang `(begin (set! x.1 5)))

'(begin (set! x.1 5))

4.1 Organizing your code to keep your graders sane

The above is a stylistic discussion about Racket best practices, some of which applies to other functional languages. However, you’re also students, and we have to grade your code.

If you’re going to refactor your code out of a single file, you must following these instructions.
  • Put all tests either in a file named a<assignment-number>-tests.rkt, or under a directory named tests.

  • Use suggestive file names, like assign-register-tests.rkt, or register-allocator.rkt, or undead-lang.rkt.

  • Create a file called README.md, which tells us in which file each exercise can be found. Make it simple and clear, like

    Exercise 2, assign-registers -> register-allocator.rkt

    Exercise 2, assign-registers tests -> tests/register-allocator-tests.rkt