functional-programminglispoption-typerobustness

Option type encoding / robustness in Lisp


(define (nth n lst)
  (if (= n 1)
    (car lst)
    (nth (- n 1)
         (cdr lst) )))

is an unsafe partial function, n may go out of range. An error can be helpful,

(define (nth n lst)
  (if (null? lst)
    (error "`nth` out of range")
    (if (= n 1)
      (car lst)
      (nth (- n 1)
           (cdr lst) ))))

But what would a robust Scheme analogue to Haskell's Maybe data type look like?

data Maybe a = Nothing | Just a

nth :: Int -> [a] -> Maybe a
nth _ []       = Nothing
nth 1 (x : _)  = Just x
nth n (_ : xs) = nth (n - 1) xs

Is just returning '() adequate?

(define (nth n lst)
  (if (null? lst) '()
    (if (= n 1)
      (car lst)
      (nth (- n 1)
           (cdr lst) ))))

Solution

  • There are several ways to do this.

    The direct equivalent would be to mimic the Miranda version:

    #!r6rs
    (library (sylwester maybe) 
      (export maybe nothing maybe? nothing?)
      (import (rnrs base))
    
      ;; private tag 
      (define tag-maybe (list 'maybe)) 
    
      ;; exported tag and features
      (define nothing (list 'nothing))
    
      (define (maybe? v)
        (and (pair? v) 
             (eq? tag-maybe (car v))))
    
      (define (nothing? v)
        (and (maybe? v)
             (eq? nothing (cdr v))))
    
      (define (maybe v)
        (cons tag-maybe v)))
    

    How to use it:

    #!r6rs
    (import (rnrs) (sylwester maybe))
    (define (nth n lst)
      (cond ((null? lst) (maybe nothing))
            ((zero? n) (maybe (car lst)))
            (else (nth (- n 1) (cdr lst)))))
    
    (nothing? (nth 2 '()))
    ; ==> #t
    

    Exceptions

    (define (nth n lst)
      (cond ((null? lst) (raise 'nth-nothing))
            ((zero? n) (car lst))
            (else (nth (- n 1) (cdr lst)))))
    
    
    (guard (ex
            ((eq? ex 'nth-nothing)
             "nothing-value"))
      (nth 1 '())) ; ==> "nothing-value"
    

    Default value:

    (define (nth n lst nothing)
      (cond ((null? lst) nothing)
                ((zero? n) (car lst))
                (else (nth (- n 1) (cdr lst)))))
    
    (nth 1 '() '()) 
    ; ==> '()
    

    Deault value derived from procedure

    (define (nth index lst pnothing)
      (cond ((null? lst) (pnothing))
            ((zero? n) (car lst))
            (else (nth (- n 1) (cdr lst)))))
    
    (nth 1 '() (lambda _ "list too short")) 
    ; ==> "list too short"
    

    Combination of exception and default procedure

    Racket, a Scheme decent, often has a default value option that defaults to an exception or a procedure thunk. It's possible to mimic that behavior:

    (define (handle signal rest)
      (if (and (not (null? rest))
               (procedure? (car rest)))
          ((car rest))
          (raise signal)))
    
    (define (nth n lst . nothing)
      (cond ((null? lst) (handle 'nth-nothing nothing)) 
            ((zero? n) (car lst))
            (else (nth (- n 1) (cdr lst)))))
    
    (nth 1 '() (lambda () 5)) ; ==> 5
    (nth 1 '()) ; exception signalled