;; An overview of exceptions ;; Consider a simple course enrollment database (require-library "function.ss") (print-struct #t) ;; student is a symbol (name) and course is a symbol (course number) (define-struct enrollrec (student course)) ;; an enrollDB is a list of enroll structures ;; create-enroll-DB : -> enrollDB (define create-enroll-DB (lambda () empty)) ;; an example DB for testing (define enrollDB (create-enroll-DB)) ;; enroll-student : student course enrollDB -> enrollDB ;; adds an enrollment of given student to given course in database (define enroll-student (lambda (student course DB) (cons (make-enrollrec student course) DB))) ;; Assume that we want to augment this program to check that students ;; only enroll in designated courses. We need a list of valid course ;; numbers and a function for checking whether a number is valid (define course-nums (list 1005 1006 2011 2135 2136 3733 3043 4431)) ;; valid-course-num? : num -> boolean ;; returns true if given number is in list of course nums (define valid-course-num? (lambda (num) (member num course-nums))) ;; Augment enroll-student to check that the given course number is valid ;; enroll-student2 : student course enrollDB -> enrollDB ;; adds an enrollment of given student to given course in database (define enroll-student2 (lambda (student course DB) (cond [(valid-course-num? course) (cons (make-enrollrec student course) DB)] [else ...]))) ;; what do we fill in for the ...? We haven't decided what to return in the ;; case that the course number is invalid. We could return some dummy value, ;; but this is bad programming practice for two reasons: ;; 1. Errors should stop a program from executing, unless the program knows ;; how to recover from the error. If the caller of this program forgets ;; to check for the dummy value, the program can continue running with ;; garbage data, which will likely cause a problem to appear far away from ;; the source of the error. Good programs report errors as soon as they ;; are known to happen. ;; ;; 2. Error checking for dummy values clutters programs. What if we also had to ;; check that the student exists, that the student is eligible to enroll for ;; the course, that the course isn't full, etc? We would end up with a lot ;; of code that checked for arbitrary dummy error values. Nothing separates ;; the error handling from the actual code, so reading the program to discover ;; what it does in normal circumstances becomes much harder. ;; Exceptions address both problems. The run-time system can distinguish exceptions ;; from normal return values. Rather than return an exception, the run-time system ;; looks for some code that knows how to recover from the error. If no such code is ;; found, the program stops running. The error-recovery code appears in a specific ;; place in a program, so it is easy to distinguish code for normal operation from ;; code for error handling. An example will make this clearer. ;; We want to fix the enroll-student2 code so that it reflects what happens in normal ;; operation. Normally, that program would confirm that a course number is valid, then ;; proceed to enroll the student. We could write the code as follows: ;; enroll-student2 : student course enrollDB -> enrollDB ;; adds an enrollment of given student to given course in database (define enroll-student2 (lambda (student course DB) (confirm-course-number-valid course) (cons (make-enrollrec student course) DB))) ;; This clarifies the normal operation of enroll-student2. confirm-course-number-valid ;; must now be a function that returns normally only if the course number is valid. In ;; other words, if the course number is invalid, the function must raise an exception ;; (in MzScheme, any value can be raised as an exception) ;; confirm-course-number-invalid : number -> void ;; returns if course number valid, otherwise raises a course-invalid exception (define confirm-course-number-valid (lambda (course) (if (valid-course-num? course) void (raise 'course-invalid-exn)))) ;; Side note: A cleaner way to write this program uses the one-armed if construct ;; "unless", which returns void if the condition is true and evaluates the given ;; expression otherwise. The dual to unless is called when. (define confirm-course-number-valid (lambda (course) (unless (valid-course-num? course) (raise 'course-invalid-exn)))) ;; enroll-student2 will now only get to execute the (cons (make-enrollrec ...)) if ;; confirm-course-number-valid returns normally. What if confirm-course-number-valid ;; raises an exception? Then we need some code that knows how to recover from that ;; problem. Assume that enroll-student2 is called from within a user-interface (define enroll-UI (lambda () (printf "Enter the course number: ") (let ([course (read)]) (printf "Enter your name: ") (let ([name (read)]) (enroll-student2 name course enrollDB))))) ;; The call to enroll-student2 may result in an exception. The UI should know ;; about that possibility, and provide a way to recover from the problem. The code ;; that recovers from errors is called an exception handler (this is true for any ;; programming language providing exceptions). In MzScheme, we install exception ;; handlers using a construct called with-handlers. (define enroll-UI2 (lambda () (printf "Enter the course number: ") (let ([course (read)]) (printf "Enter your name: ") (let ([name (read)]) (with-handlers ([(lambda (exn) (eq? exn 'course-invalid-exn)) (lambda (exn) (printf "You entered an invalid course number~n"))]) (enroll-student2 name course enrollDB)))))) ;; A with-handlers expression specifies pairs of the form ;; [predicate-to-recognize-exception function-to-handle-the-exception] ;; each of these takes one argument, namely the exception that was raised (so you can ;; store information about the system status when the exception occurred and pass that ;; to the exception handler) ;; A with-handlers expression evaluates as follows: DrScheme evaluates the code in the ;; body of the with-handlers (not the predicate/function pairs). If an exception is ;; raised while evaluating any expression in the body, DrScheme checks to see whether ;; any of the handler predicates recognize the exception. If one does, the corresponding ;; function is called, then the with-handlers expression returns normally. If no predicate ;; matches, DrScheme continues to search up the stack of called functions for a handler ;; that does accept the exception. If none exists, DrScheme reports an error (you can ;; experiment with this by running the original enroll-UI program and using an invalid ;; course number). ;; The hardest thing to learn about writing exception-based code is where to put the ;; handlers. Remember: if a handler catches an exception, DrScheme will resume execution ;; after the with-handlers expression once it processes the exception. Assume that we want ;; our enroll-UI to always print a message saying that we were exiting the enrollment ;; code. In the original version, we would have written (define enroll-UI3 (lambda () (printf "Enter the course number: ") (let ([course (read)]) (printf "Enter your name: ") (let ([name (read)]) (enroll-student2 name course enrollDB) (printf "Leaving the enrollment UI~n"))))) ;; Where do we put the with-handlers expression if we still want that last message ;; to appear even if the user gave an invalid course number? Around the enroll-student2 ;; call, but before the final printf. This guarantees that we will evaluate the printf, ;; even if an exception occurs. (define enroll-UI4 (lambda () (printf "Enter the course number: ") (let ([course (read)]) (printf "Enter your name: ") (let ([name (read)]) (with-handlers ([(lambda (exn) (eq? exn 'course-invalid-exn)) (lambda (exn) (printf "You entered an invalid course number~n"))]) (enroll-student2 name course enrollDB)) (printf "Leaving the enrollment UI"))))) ;; In contrast, putting the printf inside the with-handlers would mean that we would only ;; see the exit message if a valid course number was entered. (define enroll-UI5 (lambda () (printf "Enter the course number: ") (let ([course (read)]) (printf "Enter your name: ") (let ([name (read)]) (with-handlers ([(lambda (exn) (eq? exn 'course-invalid-exn)) (lambda (exn) (printf "You entered an invalid course number~n"))]) (enroll-student2 name course enrollDB) (printf "Leaving the enrollment UI")))))) ;; Experiment with both pieces of code to make sure you understand the difference ;; Finally, what if we wanted to catch multiple kinds of exceptions from enrolling students? ;; We could add a handler for 'course-full-exn as follows (define enroll-UI6 (lambda () (printf "Enter the course number: ") (let ([course (read)]) (printf "Enter your name: ") (let ([name (read)]) (with-handlers ([(lambda (exn) (eq? exn 'course-invalid-exn)) (lambda (exn) (printf "You entered an invalid course number~n"))] [(lambda (exn) (eq? exn 'course-full-exn)) (lambda (exn) (printf "The course is full~n"))]) (enroll-student2 name course enrollDB)) (printf "Leaving the enrollment UI"))))) ;; Final note: I prefer to give names to my handler predicates to make the code easier ;; to read. For example: (define course-invalid-exn? (lambda (value) (eq? value 'course-invalid-exn))) (define course-full-exn? (lambda (value) (eq? value 'course-full-exn))) (define enroll-UI7 (lambda () (printf "Enter the course number: ") (let ([course (read)]) (printf "Enter your name: ") (let ([name (read)]) (with-handlers ([course-invalid-exn? (lambda (exn) (printf "You entered an invalid course number~n"))] [course-full-exn? (lambda (exn) (printf "The course is full~n"))]) (enroll-student2 name course enrollDB)) (printf "Leaving the enrollment UI")))))