I am reading The Seasoned Schemer by Friedman and Felleisen, but I am a little uneasy with some of their best practices. In particular, the authors recommend:
- using
letrec
to remove arguments that do not change for recursive applications; - using
letrec
to hide and to protect functions; - using
letcc
to return values abruptly and promptly.
Let's examine some consequences of these rules. Consider for example this code for computing the intersection of a list of lists:
#lang scheme
(define intersectall
(lambda (lset)
(let/cc hop
(letrec
[[A (lambda (lset)
(cond [(null? (car lset)) (hop '())]
[(null? (cdr lset)) (car lset)]
[else (I (car lset) (A (cdr lset)))]))]
[I (lambda (s1 s2)
(letrec
[[J (lambda (s1)
(cond [(null? s1) '()]
[(M? (car s1) s2) (cons (car s1) (J (cdr s1)))]
[else (J (cdr s1))]))]
[M? (lambda (el s)
(letrec
[[N? (lambda (s)
(cond [(null? s) #f]
[else (or (eq? (car s) el) (N? (cdr s)))]))]]
(N? s)))]]
(cond [(null? s2) (hop '())]
[else (J s1)])))]]
(cond [(null? lset) '()]
[else (A lset)])))))
This example appears in Chapter 13 (not exactly like this: I glued the membership testing code that is defined separately in a previous paragraph).
I think the following alternative implementation, which makes very limited use of letrec
and letcc
is much more readable and simpler to understand:
(define intersectall-naive
(lambda (lset)
(letrec
[[IA (lambda (lset)
(cond [(null? (car lset)) '()]
[(null? (cdr lset)) (car lset)]
[else (intersect (car lset) (IA (cdr lset)))]))]
[intersect (lambda (s1 s2)
(cond [(null? s1) '()]
[(M? (car s1) s2) (cons (car s1) (intersect (cdr s1) s2))]
[else (intersect (cdr s1) s2)]))]
[M? (lambda (el s)
(cond [(null? s) #f]
[else (or (eq? (car s) el) (M? el (cdr s)))]))]]
(cond [(null? lset) '()]
[else (IA lset)]))))
I am new to scheme and my background is not in Computer Science, but it strikes me that we have to end up with such complex code for a simple list intersection problem. It makes me wonder how people manage the complexity of real-world applications.
Are experienced schemers spending their days deeply nesting letcc
and letrec
expressions?
This was the motivation for asking stackexchange.
My question is: are Friedman and Felleisen overly complicating this example for education's sake, or should I just get accustomed to scheme code full of letcc
s and letrec
s for performance reasons?
Is my naive code going to be impractically slow for large lists?