Boolean operators tail-call optimized?

时间:2015-09-14 16:13:13

标签: stack-overflow racket boolean-operations tail-call-optimization

While learning Racket and getting into programming in general I was defining ormap in two different ways:

(define (or-map proc lst)
  (cond
    [(null? lst) #false]
    [(proc (car lst)) #true]
    [else (or-map proc (cdr lst))]))


(define (or-map proc lst)
  (if (null? lst)
      #false
      (or (proc (car lst)) ; <-- this one
          (or-map proc (cdr lst)))))

The following questions came to mind:

Is the second one tail-call optimized? I was not sure if the commented line gets thrown away (or the (or ...) stacks up it's arguments), because if it's #true the function call ends and if it's #false it should be irrelevant for further evaluation of the (or ...) statement.

So I ran the following test and looked at the task manager for memory usage:

(define (test)
  (or #f
      (test)))

(test)

The memory stayed the same. So I think I can conclude (or ...*) gets tail-call optimized? I assumed that stays true for (and ...*) and other boolean operators, but as I changed the or in (test) to nor the memory filled up.

So all in all, do I

  • have some mistakes in my conclusions thus far?
  • what's going on with the nor-test?
  • rightfully assume both of my definitions of or-map are equivalent performance wise, or is one preferable over the other?
  • (is my use of the task manager in this case even legitamate and is the phenomenon I witness there when the memory fills up stackoverflow or a implication thereof?)

3 个答案:

答案 0 :(得分:3)

鉴于您的用户名为Tracer,您可能会发现使用racket/trace来检查这一点很有趣。 :)

首先是您希望使用尾部消除的函数示例,而不是:

#lang racket

(define (tail-call xs [results '()])
  (match xs
    [(list) results]
    [(cons x xs) (tail-call xs (cons x results))]))

(define (no-tail-call xs)
  (match xs
    [(list) (list)]
    [(cons x xs) (cons x (no-tail-call xs))]))

我们可以trace他们,并在调用深度中看到这一点:

(require racket/trace)
(trace tail-call no-tail-call)

(tail-call '(1 2 3 4 5))
;; >(tail-call '(1 2 3 4 5))
;; >(tail-call '(2 3 4 5) '(1))
;; >(tail-call '(3 4 5) '(2 1))
;; >(tail-call '(4 5) '(3 2 1))
;; >(tail-call '(5) '(4 3 2 1))
;; >(tail-call '() '(5 4 3 2 1))
;; <'(5 4 3 2 1)
;; '(5 4 3 2 1)

(no-tail-call '(1 2 3 4 5))
;; >(no-tail-call '(1 2 3 4 5))
;; > (no-tail-call '(2 3 4 5))
;; > >(no-tail-call '(3 4 5))
;; > > (no-tail-call '(4 5))
;; > > >(no-tail-call '(5))
;; > > > (no-tail-call '())
;; < < < '()
;; < < <'(5)
;; < < '(4 5)
;; < <'(3 4 5)
;; < '(2 3 4 5)
;; <'(1 2 3 4 5)
;; '(1 2 3 4 5)

接下来让我们为or-map的两个定义执行此操作。我们看到两者都是相同的扁平形状:

(define (or-map/v1 proc lst)
  (cond
    [(null? lst) #false]
    [(proc (car lst)) #true]
    [else (or-map/v1 proc (cdr lst))]))

(define (or-map/v2 proc lst)
  (if (null? lst)
      #false
      (or (proc (car lst)) ; <-- this one
          (or-map/v2 proc (cdr lst)))))

(trace or-map/v1 or-map/v2)

(or-map/v1 even? '(1 1 1 2))
;; >(or-map/v1 #<procedure:even?> '(1 1 1 2))
;; >(or-map/v1 #<procedure:even?> '(1 1 2))
;; >(or-map/v1 #<procedure:even?> '(1 2))
;; >(or-map/v1 #<procedure:even?> '(2))
;; <#t
;; #t

(or-map/v2 even? '(1 1 1 2))
;; >(or-map/v2 #<procedure:even?> '(1 1 1 2))
;; >(or-map/v2 #<procedure:even?> '(1 1 2))
;; >(or-map/v2 #<procedure:even?> '(1 2))
;; >(or-map/v2 #<procedure:even?> '(2))
;; <#t
;; #t

答案 1 :(得分:2)

andor会在尾部位置评估他们的最后一个表达式。这是由Scheme标准保证的;例如,见http://www.r6rs.org/final/html/r6rs/r6rs-Z-H-14.html#node_sec_11.20

另一方面,

nor必须否定or的结果。根据定义,这意味着or的结果不会在尾部位置进行评估,因为在返回调用者之前必须将其传递给not

答案 2 :(得分:1)

你在问:

  

理所当然地假设我对or-map的两个定义都是等效的,或者比另一个更优先?

但请注意,您的第一个函数等效于第二个函数(不会产生相同的结果)。如果您通过以下方式调用它们,则可以验证这一点:

(or-map (lambda (x) (member x '(1 2 3))) '(1 2 a))

原因是,当#true返回与(proc (car lst))不同的内容时,在第一个函数中返回#false,但该函数应返回(proc (car lst))的值。所以“正确”版本(相当于Racket ormap的版本)只是第二个版本。