Lisp 中的选项类型编码/健壮性

Option type encoding / robustness in Lisp

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

是不安全的偏函数,n 可能超出范围。 error 可能会有帮助,

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

但是与 Haskell 的 Maybe 数据类型类似的 稳健 方案会是什么样子?

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

只返回 '() 就足够了吗?

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

很容易打破你的尝试。只需创建一个包含空列表的列表:

(define lst '((1 2) () (3 4)))
(nth 2 lst)
-> ()
(nth 100 lst)
-> ()

您遗漏的关键点是 Haskell 的 Maybe 不只是 return 存在时的裸值,它包含该值。如您所说,Haskell 定义 Maybe 如下:

data Maybe a = Nothing | Just a

不是这样的:

data Maybe a = Nothing | a

后者相当于你正在做的事情。

为了获得正确 Maybe 的大部分方法,如果元素不存在,您可以 return 空列表,就像您一样,但也 如果元素确实存在,则将 return 值包装在另一个列表中:

(define (nth n lst)
  (if (null? lst) '()
    (if (= n 1)
      (list (car lst)) ; This is the element, wrap it before returning.
      (nth (- n 1)
           (cdr lst) ))))

这样,您的结果要么是一个空列表,这意味着该元素不存在,要么是一个只有一个元素的列表:您要求的元素。从上面重复使用相同的列表,我们可以区分空列表和不存在的元素:

(define lst '((1 2) () (3 4)))
(nth 2 lst)
-> (())
(nth 100 lst)
-> ()

另一种表示未找到匹配元素的方法是使用多个 return 值:

(define (nth n ls)
  (cond
    ((null? ls) (values #f #f))
    ((= n 1) (values (car ls) #t))
    (else (nth (- n 1) ls))))

这是以对使用该功能的用户来说有点麻烦为代价的,因为他们现在必须做一个

(call-with-values (lambda () (nth some-n some-list))
                  (lambda (element found?)
                     ... whatever ...))

但这可以通过使用一些谨慎的宏观方法来缓解。 R7RS 指定 let-values 语法。

(let-values (((element found?) (nth some-n some-list)))
   ... whatever ...)

作为一个非 lisper 我真的不能说这是多么地道,但你可以 return 选项类型的 Church encoding:

(define (nth n ls)
  (cond
    ((null? ls) (lambda (default f) default))
    ((= n 1) (lambda (default f) (f (car ls))))
    (else (nth (- n 1) ls))))

但这与@Dirk 的提议一样复杂。我个人更喜欢为 nth 本身添加一个默认参数。

有几种方法可以做到这一点。

直接等同于模仿 Miranda 版本:

#!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)))

使用方法:

#!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

例外情况

(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"

默认值:

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

(nth 1 '() '()) 
; ==> '()

从过程派生的默认值

(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"

异常和默认过程的组合

Racket,一个体面的Scheme,通常有一个默认值选项,默认为一个异常或一个过程thunk。可以模仿这种行为:

(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