2010年3月24日水曜日

TSS scramble は末尾再帰可能

valvallowさんのブログ見てたんですけど、The Seasoned Schemerscrambleって全然意味不明ですな~~。困ったもんだ。
valvallowさんが抜粋したscrambleのコードですが、ちょっと見てみますか。

(define sub1
(lambda (n)
(- n 1)))

(define pick
(lambda (n lat)
(list-ref lat (sub1 n))))

(define scramble
(lambda (tup)
(letrec ((iter (lambda (t rev-pre)
(if (null? t)
'()
(let* ((n (car t))
(rev (cons n rev-pre)))
(cons (pick n rev)
(iter (cdr t) rev)))))))
(iter tup '()))))

ふ~む。何やってんだかサッパリです(苦笑)。
基本的にletrecがいけねえんだな(苦笑)。見づらい。
と言うわけでnamed-letに書き換えてみます。そして個人的にはあまりlet*が好きじゃないんで(何故、って訊かないで・笑)、そこも直して、pickも敢えて解体して書き直してみます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()))
(if (null? t)
'()
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(cons (list-ref rev (- n 1))
(iter (cdr t) rev)))))))

ふ~~~む。まだ何やってるか分かり辛いですよね。しかも、このスタイルは末尾再帰じゃない。何か末尾再帰出来そうな気がするんですよね~~。
コードの意味が分からん、かつ末尾再帰になりそう、と思った時どうすっか。一番簡単なのは継続渡し方式に取りあえず書き直してみる事です。ひょっとしたら、これで何か見えてくるかもしれません。やってみます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (k values))
(if (null? t)
(k '())
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev
(lambda (u)
(k (cons (list-ref rev (- n 1)) u)))))))))

んで、詳しい論理の流れはこのページを参考にして欲しいんですけど、上のコードは次のように書き換えられて、

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()) (k values))
(if (null? t)
(k (reverse acc))
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc)
(lambda (u) (k u))))))))

ついでに次のように書き換えられます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()) (k values))
(if (null? t)
(k (reverse acc))
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc) k))))))

ここまで来ると、もはやローカル手続きのkが要らなくなります。事実上、全く仕事をしてないから、です。
従って、題意のコードは、

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()))
(if (null? t)
(reverse acc)
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc)))))))

となりますね。末尾再帰になりました。
末尾再帰の方が若干何やってるか分かりやすいかもしれません。

  1. n番目の指定はリストtの先頭の数字によって決定される。

  2. リストrev(cons n rev)で生成される。また、このリストは常にリストtに対しては部分的に逆順となっている。

  3. リストaccはリストrevn番目の要素をconsしていって生成される。

  4. t(cdr t)となり(null? t) => #tとなるまで1番から作業を繰り返す。

  5. 返り値はaccを逆順にしたものである。


まだ、ちょっと分かり辛いかもしれませんね。例えば、

  1. t(1 1 1 3 4 2 1 1 9 2)revは空リスト、accも空リストとする。

  2. nは1となり、rev(1)となる。

  3. revn = 1番目の要素は1なので、acc(1)となる。

  4. tは(1 1 3 4 2 1 1 9 2)となり、以下繰り返し。


つまり、キーポイントとしては「常にtに対して部分的には逆順のリストであるrevn番目の要素を問題にしている」と言う事なんです。
まだ分かり辛いかもしれないんで、valvallowさんの真似をして出力部分を組み合わせてみます。

(define (scramble tup)
(let iter ((t tup) (rev-pre '()) (acc '()))
(if (null? t)
(reverse acc)
(let ((n (car t)))
(let ((rev (cons n rev-pre)))
(for-each (lambda (x) ; ここは出力
(display x))
(list "t = " t ", n = " n ", rev = " rev ", acc = " acc))
(newline) ; 改行
(iter (cdr t) rev (cons (list-ref rev (- n 1)) acc)))))))

出力結果は以下の通りです。

> (scramble '(1 1 1 3 4 2 1 1 9 2))
t = (1 1 1 3 4 2 1 1 9 2), n = 1, rev = (1), acc = ()
t = (1 1 3 4 2 1 1 9 2), n = 1, rev = (1 1), acc = (1)
t = (1 3 4 2 1 1 9 2), n = 1, rev = (1 1 1), acc = (1 1)
t = (3 4 2 1 1 9 2), n = 3, rev = (3 1 1 1), acc = (1 1 1)
t = (4 2 1 1 9 2), n = 4, rev = (4 3 1 1 1), acc = (1 1 1 1)
t = (2 1 1 9 2), n = 2, rev = (2 4 3 1 1 1), acc = (1 1 1 1 1)
t = (1 1 9 2), n = 1, rev = (1 2 4 3 1 1 1), acc = (4 1 1 1 1 1)
t = (1 9 2), n = 1, rev = (1 1 2 4 3 1 1 1), acc = (1 4 1 1 1 1 1)
t = (9 2), n = 9, rev = (9 1 1 2 4 3 1 1 1), acc = (1 1 4 1 1 1 1 1)
t = (2), n = 2, rev = (2 9 1 1 2 4 3 1 1 1), acc = (1 1 1 4 1 1 1 1 1)
(1 1 1 1 1 4 1 1 1 9)
> (scramble '(1 2 3 4 5 6 7 8 9))
t = (1 2 3 4 5 6 7 8 9), n = 1, rev = (1), acc = ()
t = (2 3 4 5 6 7 8 9), n = 2, rev = (2 1), acc = (1)
t = (3 4 5 6 7 8 9), n = 3, rev = (3 2 1), acc = (1 1)
t = (4 5 6 7 8 9), n = 4, rev = (4 3 2 1), acc = (1 1 1)
t = (5 6 7 8 9), n = 5, rev = (5 4 3 2 1), acc = (1 1 1 1)
t = (6 7 8 9), n = 6, rev = (6 5 4 3 2 1), acc = (1 1 1 1 1)
t = (7 8 9), n = 7, rev = (7 6 5 4 3 2 1), acc = (1 1 1 1 1 1)
t = (8 9), n = 8, rev = (8 7 6 5 4 3 2 1), acc = (1 1 1 1 1 1 1)
t = (9), n = 9, rev = (9 8 7 6 5 4 3 2 1), acc = (1 1 1 1 1 1 1 1)
(1 1 1 1 1 1 1 1 1)
> (scramble '(1 2 3 1 2 3 4 1 8 2 10))
t = (1 2 3 1 2 3 4 1 8 2 10), n = 1, rev = (1), acc = ()
t = (2 3 1 2 3 4 1 8 2 10), n = 2, rev = (2 1), acc = (1)
t = (3 1 2 3 4 1 8 2 10), n = 3, rev = (3 2 1), acc = (1 1)
t = (1 2 3 4 1 8 2 10), n = 1, rev = (1 3 2 1), acc = (1 1 1)
t = (2 3 4 1 8 2 10), n = 2, rev = (2 1 3 2 1), acc = (1 1 1 1)
t = (3 4 1 8 2 10), n = 3, rev = (3 2 1 3 2 1), acc = (1 1 1 1 1)
t = (4 1 8 2 10), n = 4, rev = (4 3 2 1 3 2 1), acc = (1 1 1 1 1 1)
t = (1 8 2 10), n = 1, rev = (1 4 3 2 1 3 2 1), acc = (1 1 1 1 1 1 1)
t = (8 2 10), n = 8, rev = (8 1 4 3 2 1 3 2 1), acc = (1 1 1 1 1 1 1 1)
t = (2 10), n = 2, rev = (2 8 1 4 3 2 1 3 2 1), acc = (2 1 1 1 1 1 1 1 1)
t = (10), n = 10, rev = (10 2 8 1 4 3 2 1 3 2 1), acc = (8 2 1 1 1 1 1 1 1 1)
(1 1 1 1 1 1 1 1 2 8 2)
>

2010年3月15日月曜日

TSS rember1*


ネストした cond とか読みづらいですねー。

読みづらい、どころの問題じゃないです(苦笑)。
問題のコードはこれなんですけど。中でも「一番マシ」なのをピックアップしました。

; The Seasoned Schemer letrec, let
(define rember1*
(lambda (a l)
(letrec
((R (lambda (l)
(cond
((null? l)(quote ()))
((atom? (car l))
(cond
((eq? (car l) a)(cdr l))
(else (cons (car l)
(R (cdr l))))))
(else (let ((av (R (car l))))
(cond
((eqlist? (car l) av)
(cons (car l)(R (cdr l))))
(else (cons av (cdr l))))))))))
(R l))))

(rember1* 'salad '((Swedish rye)
(French (mustard salad turkey))
salad))
; -> ((Swedish rye) (French (mustard turkey)) salad)

もうあったまクラクラしてきますね(笑)。Schemeにある程度慣れていてもクラクラしてくるコードです。
The Little SchemerThe Seasoned Schemerは「名著」の誉れが高いんですが「ホンマか?」と頭の中が疑念の嵐です、しょーじきなトコロ。ひっどいコードの垂れ流しとしか見えません。
僕は両書とも持ってないんで、意図がどこにあるのか、あるいは何か目的があってそこへ誘おうとしてるのか分からないんですが、いずれにせよ、このコードを見る以上「酷いコードが書かれてあるテキスト」だとしか思えません。

「リファクタリングせねば!」

とか思ったんですが、これがなかなか(苦笑)。一筋縄ではいかない。
valvallowさんが記述している通り、

ネストした list 内から最初に見つかったものを一つだけ取り除くって、一つだけって逆に難しいですね。

なんです。全くです(苦笑)。

しかしながら、テキストを鵜呑みにするんじゃなくって

「何とかリファクタリングして短いコードが書けないか?」

と考えるのは良い練習だと思います。
と言うのも、Schemeで実用的なプログラムを書くのは難しいですし、もしある目的を達成するコードが「短く書けない」のなら、そもそもSchemeなんて勉強する必要なんて無いんです。

コードを極限まで短くする方法を考える

のがSchemeの学習の唯一の価値です。ホントですよ。
(大体、短く書けなきゃSchemeはやたら括弧が多い妙ちきりんな言語なだけ、です。)

まず、例示のコードを見て気になるのは、letrecを用いている意味が全くない、って辺りです。
と言うのも、このコードは全く末尾再帰してないので、ローカル手続きを使ってる意味が無いんです。従って、まずはletrecを消去しちゃいましょう。ここは無駄な部分です。

(define (rember1* a l)
(cond
((null? l) '())
;; atom? は標準手続きじゃないので、not pair? に置き換える。
((not (pair? (car l)))
(cond
((eq? (car l) a) (cdr l))
(else (cons (car l) (rember1* a (cdr l))))))
(else (let ((av (rember1* a (car l))))
(cond
;; eqlist?も標準手続きではない。
;; リスト同士の等価判定はequal?で充分。
((equal? (car l) av)
(cons (car l) (rember1* a (cdr l))))
(else (cons av (cdr l))))))))

それで、このコードを扱ってる章では、valvallowさんの記述によると、

letを使って局所変数を纏めよう

と言うのが狙いのようなんですが、だったらどうしてこんなに中途半端なのか。コード中に現れる(car l)(cdr l)こそ局所変数で纏めるべきなんですよ。これが素のままで記述されているので、ネストだらけになって見づらいんです。

(define (rember1* a l)
(if (null? l)
'()
(let ((x (car l)) (y (cdr l)))
(if (not (pair? x))
(if (eq? x a)
y
(cons x (rember1* a y)))
(let ((av (rember1* a x)))
(if (equal? x av)
(cons x (rember1* a y))
(cons av y)))))))

valvallowさんが仰ってる通り、

分岐が2つなら if 使おうよ。

って事なんで、condも全部ifに置き換えます。
ちょっとは見通し良くなったかな?

次は個人的趣味なんですが、あんまnot~ってのは好きじゃないんですよね。元々分岐が二つである以上、論理的には「pair?であるか、そうじゃないか」ってだけです。つまり、分岐二つの枝の順序は取り替えて良い。

(define (rember1* a l)
(if (null? l)
'()
(let ((x (car l)) (y (cdr l)))
(if (pair? x)
(let ((av (rember1* a x)))
(if (equal? x av)
(cons x (rember1* a y))
(cons av y)))
(if (eq? x a)
y
(cons x (rember1* a y)))))))

だいぶ整理されて見やすくなっては来ました。だけど依然としてifのネストはウザいです。ウザいですね。
一つの考え方ですが、例えば(pair? x)以降のifのカタマリに着目してみましょうか。
Lisp系言語の素晴らしいトコは、文がないと言う辺りです。実際文がある言語ではif~then~elseは制御構文なんで、基本的に手続きの引数につっこめません。必ず手続きの外側に置く必然性が出てきます。
一方、全てが式であるLispではその辺融通無碍です。
どう言う事かと言うと、この部分ではどっちにせよ動作は

(cons 何とか1 何とか2)

が求められてるんですね。フォーマットとしては特に差異がないんです。
つまり、次のように書き換えてしまう事が可能です。

(define (rember1* a l)
(if (null? l)
'()
(let ((x (car l)) (y (cdr l)))
(if (pair? x)
(let ((av (rember1* a x)))
;; 述語の判定結果を it に束縛してしまう。
(let ((it (equal? x av)))
;; it を利用して cons の引数を交換してしまう。
(cons (if it x av) (if it (rember1* a y) y))))
(if (eq? x a)
y
(cons x (rember1* a y)))))))

これは荒技に見えるかもしれませんが、結構Lisp系ではオーソドックスな手だと思います。これでネストが減ったように見えて(実際見えるだけ、ですが)若干コードがフラットになった印象を受けます。
そして、

(cons 何とか1 何とか2)

(let ((x (car l)) (y (cdr l)))...以下の主作業であり、また「引数交換」だけで書けるのなら、最後の枝も纏めてしまえるのでは、と言う野望が生まれますね。そして恐らくそれは正しいんです。
ちょっと整理してみましょうか。

  1. (eq? x a)が成り立った時点で、xは(Schemeの仕様上そう言う定義は無いが)アトムである事が決定している。-> つまりこれは独立して括り出せる。

  2. 残りはpair? => #tかアトムであるけどeq? => #fじゃないか、の二つの枝である。

  3. (cons 何とか1 何とか2)の中身が部分的に重複しているパターンが多い


の3点が分かりますね。実際そうなんです。
1番目を考慮すると、コードは取りあえず次のように書けば良いのが分かります。

(define (rember1* a l)
(if (null? l)
'()
(let ((x (car l)) (y (cdr l)))
(if (eq? x a)
y
...)

これで1番目は綺麗に処理されます。あとはpair?の中身のパターンわけをどうするのか、って事なんですけど……。並べて見てみますか。

(cons (if it x av) (if it (rember1* a y) y))
(cons x (rember1* a y))

第一引数に着目します。候補の引数の中身はxavの二種類の可能性しかない、って事がお分かりでしょう。当たり前ですよね。
そして、もう一つ重要なのは、xpair? => #t「だからこそ」再帰でrember1*を噛ます必要性があるのであって、そうじゃなければそんな事する必要がない、のです。そして、再帰噛ました結果とxが等価であるかどうかは第一引数に付いてはロジック上丸っきり関係がありません。その判定が必要なのは第二引数の方なのです。
従って、第一引数は一つのシンボルavで表現して、avはSchemeの有難いif式を利用して束縛します。

(define (rember1* a l)
(if (null? l)
'()
(let ((x (car l)) (y (cdr l)))
(if (eq? x a)
y
;; av を次のような条件式で束縛する。
(let ((av (if (pair? x) (rember1* a x) x)))
...)

これでxpair? => #tだったらav(rember1* a x)に束縛されるし、そうじゃなかったらxに束縛されます。
これにより、ここのletの本体は

(cons av ...)

と書ける事が決定しました。他に何も必要がないのです。こう言う大技が繰り出せるのも、Schemeが文を持たないお陰です。有難や有難や。
ではconsの第二引数はどうなんでしょうか?これは(equal? x av) => #fの「時だけ」yである必要があり、デフォルトでは(rember1* a y)である、ってのが条件なんです。
従って、完成コードは、

(define (rember1* a l)
(if (null? l)
'()
(let ((x (car l)) (y (cdr l)))
(if (eq? x a)
y
(let ((av (if (pair? x) (rember1* a x) x)))
(cons av (if (equal? x av) (rember1* a y) y)))))))

となりました。随分とスッキリしたでしょ?ネストが減ったように見えて、結構フラットな印象になったと思います。また、コードの「意味」も追いやすくなったんじゃないかな、と思います。

2010年3月1日月曜日

call/ccの冒険~その2

call/ccの意味の分からないトコは、「無くてもいいじゃん?」って思えるケースがしばしば見受けられるトコです。
この原因の主たるところは、Schemeは明示的にreturnを使うようにはなっていない為じゃないのか、とかちょっと思うんです。
割に、C言語や、その派生モデルの言語の方がcall/ccの意図がつかみ易いのでは、と時々思います。
いずれにせよ、R5RSに以下のように記述されている通り、

call-with-current-continuation の平凡な用途は,ループまたは手続き本体からの構造化された非局所的脱出である。

call/ccの取りあえずの目的は脱出なんです。いわゆるcall/ccの「すげえ機能」と言う一種の誤解は、この「脱出をどう実現してるのか」から出てきた言わば副産物なんですよね。
従って、call/ccで脱出にこだわるのは、call/ccの機能を知る為の第一歩なんじゃないか、と思います。

ところで。ANSI Common LispとSchemeには次のような違いがあります。
ANSI Common Lispの場合、

CL-USER> (car '())
NIL
CL-USER> (cdr '())
NIL
CL-USER>

となります。
これは、たまにプログラミングに於いてバグになるんですが、上手くすれば非常に便利なんですよね。コードを書く量が劇的に減ります。
実はこれはInterlispと言うLispから受け継いだらしいんですが、はじめは「何て手抜きな事を」と感じる事もなきにしもあらず。しかし、数学的には正しい結果でもあるんです。

空集合の部分集合は空集合自身のみである。

いいですね。魅力的です。
ただし、Schemeは数学的に見てヘンな結果になります。端的に言うとエラーを返す。

> (car '())
car: expects argument of type ; given ()

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

> (cdr '())
cdr: expects argument of type ; given ()

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>

ここで、Scheme上でANSI Common Lispのような働きをするextended-carとextended-cdrを書いてみましょう。それぞれxcar、xcdrとします。
恐らく、オーソドックスにSchemeでこれらを書くと、次のようなコードになるでしょう。

(define (xcar lst)
(if (null? lst)
lst
(car lst)))

(define (xcdr lst)
(if (null? lst)
lst
(cdr lst)))

何にも問題ありませんね。至極単純なコードです。実行結果は以下の通り。

> (xcar '())
()
> (xcar '(1 2 3 4 5))
1
> (xcdr '())
()
> (xcdr '(1 2 3 4 5))
(2 3 4 5)
>

敢えてこれをcall/ccを用いて記述してみましょう。コードは次のようになります。

(define (xcar lst)
(call/cc
(lambda (return)
(car (if (null? lst)
(return lst)
lst)))))

(define (xcdr lst)
(call/cc
(lambda (return)
(cdr (if (null? lst)
(return lst)
lst)))))

これが脱出です。
考え方としては、要するに普通のcar/cdrなんですが、lstが空リストだった場合エラーになるんで、その条件の場合、car/cdrが作り出す筈の「計算結果を捨てて」空リストを持って脱出するわけです。
これは普通の手続き型言語で考えたらお馴染みでしょう。簡単です。
ただし、

  1. call/ccを使わないでも書けるコードなのに、call/ccを使う必然性が見当たらない。

  2. 単に構文的にはreturnがあれば済む話なんじゃないか。

  3. そもそも、単に脱出する為だけに(call/cc (lambda (hoge) ...))って大掛かりさは何なんだ。


と言う不満があるでしょう。全くもってその通りです。
僕は1番目にずーっと引っかかってたんですよね。call/ccを使わなくても記述可能な場合が多い、んです。他でもっと簡易な代替手段があるのに、それでも敢えてcall/ccを使わなきゃならない理由は何なんだ、と。全くもって意味不明でした。
手続き型言語をやってる人は2番3番は思うでしょう。実際ANSI Common Lispにはreturnとかreturn-fromと言う構文があるんで、call/ccの煩雑さは際立ちます。単に脱出するだけだったらもっと簡易な脱出構文を用意しとけばいいのです。抽象化の失敗の薫りがしますね。
しかし、call/ccの練習、つまり、「慣れ」を考えるなら、敢えて上記のように「call/ccを使わなくても」書ける手続きをcall/ccを使って記述しなおすのは良いと思います。少なくと上の例のように「括弧の中と外が」ひっくり返るような形になるでしょう。文字通りひっくり返るんです。ここ、重要ですよ。

実は、この手の処理の場合、独立した手続きにするより、何かの組み込みとしてcall/ccを使った方が有用性が際立ちます。特に空リストのcarを取ったり、とかcdrを取ってエラーになる、と言うのは、ロジック自体は間違ってないトコでの「Scheme特有の性質」に足を掬われたりするんで、効果は絶大です。

ところで、ANSI Common Lispにはeveryと言う関数が定義されています。述語とシーケンスを引数に取り、シーケンスの全要素が述語を満たすかどうか判定します。
使用例は以下の通り。

CL-USER> (every #'characterp "abc")
T
CL-USER> (every #'oddp '(1 3 5))
T
CL-USER> (every #'> '(1 3 5) '(0 2 4))
T
CL-USER>

これをSchemeで定義してみます。もちろんオーソドックスには再帰で繰り返し構造を記述して…と言うのが手の一つですが、メンド臭いんで、高階手続きを使って以下のようなコードをでっち上げます。

(define (every pred . arg)
(not (member #f
(apply map pred
(map (lambda (x)
(cond ((string? x) (string->list x))
((vector? x) (vector->list x))
(else x))) arg)))))

ポイントがいくつかあります。

  1. レストパラメータ以降の引数は全て一つのリストに纏められる。

  2. Schemeでリストに変換可能なデータ型(シーケンス)はR5RS範疇では文字列とベクタである。

  3. 写像手続きmapは結果をリストとして返す。

  4. 高階手続きapplyは最後の引数がリストであれば、何を適用しても良い。

  5. 手続きmemberは第一引数が第二引数のリストに含まれてるか調べる。

  6. Schemeでは空リストも含み、論理的にはリストは#tである。


繰り返しを地道に書くのはメンド臭いです。メンド臭いからこそ写像手続き、あるいは高階手続きが存在します。
論理的には上のような流れで、極めて簡単にSchemeでeveryが実装出来ます。動作も完璧ですよね。

> (every char? "abc")
#t
> (every odd? '(1 3 5))
#t
> (every > '(1 3 5) '(0 2 4))
#t
>

ってなワケで完成しました。ロジックも穴がありません。
「動けば良い」のならこれでよい。でも、この富豪の時代ではあまり気にならないんですが(正直気にならない)このコードは実は「効率性」から言うと問題があるんです。
上のコードでは手続きmemberを用いて与えられたリスト(apply以降の計算結果)に#fが含まれるかどうか見ています。問題は「1個でも#fが見つかれば、everyと言う定義に反する」って部分なんです。
つまり、計算途中に#fが見つかればそれで既に論理的には結果は明らかに#fなんです。そして、それ以降が#tだろうが#fだろうがどーでも良いのです。しかし、上のコードは律儀に最後までリストを走査します。
もうちょっと細かくapply以降の動作を見てみましょう。

(define (engine-of-every pred . arg)
(apply map pred
(map (lambda (x)
(cond ((string? x) (string->list x))
((vector? x) (vector->list x))
(else x))) arg)))

;; 実行例
> (engine-of-every odd? '(1 2 3 4 5))
(#t #f #t #f #t)
> (engine-of-every > '(1 2 5) '(0 3 4))
(#t #f #t)
>

上の実行例を見ても分かると思いますが、1つ目の例で言うとリストの2個目の要素が#fだと分かった以上、それ以降の計算は無意味です。2つ目の例でもそうですね。その時点で計算を終わらせればそれでいいのです。
こう言う場合、強制的に計算を終了させるのがcall/ccの役目で、それこそが「脱出」です。call/ccを用いればもうちょっと計算効率が上がります。

(define (engine-of-every pred . arg)
(call/cc
(lambda (return)
(apply map
(lambda x ;ここはR5RS参照
(or (apply pred x)
(return #f))) ;脱出
(map (lambda (y)
(cond ((string? y) (string->list y))
((vector? y) (vector->list y))
(else y))) arg)))))

;; 実行例
> (engine-of-every char? "abc")
(#t #t #t)
> (engine-of-every odd? '(1 2 3 4 5))
#f
> (engine-of-every > '(1 3 5) '(0 2 4))
(#t #t #t)
> (engine-of-every > '(1 2 5) '(0 3 4))
#f
>

実はもう殆ど完成してますね。論理的には(#t #t #t)だろうと他のリストだろうと#tです。そして、一個でも#fが見つかった時には即座に計算を終了して#fを持って脱出してます。
このように、結構高階手続きとcall/ccは相性が良いです。高階手続きを設計する際は「call/ccを使う余地が無いか?」考えてみるのが一興でしょう。
なお、上のengine-of-everyで唯一注釈が必要なのは、R5RSに記述されている次のラムダ式の記法でしょう。

<変数>: 手続きは任意個数の引数をとる。手続きが呼び出される時には,実引数の列が,一つの新しく割り付けられたリストへと変換され,そしてそのリストが<変数> の束縛に格納される。

((lambda x x) 3 4 5 6) =⇒ (3 4 5 6)


一種、ラムダ式のレストパラメータと言っても良いのですが、ラムダ式の仮引数が括弧で囲まれてない場合、与えられた引数はリストとして纏められる、と言うルールがあります。あまり有名ではないのですが、覚えておいて損はないでしょう。

実は前回のコードの継続2と言うのは単にこの脱出を行っています。
もう一回コードを再録してみます。見比べてみてください。

(define (sum n)
(let ((tag #f)) ;タグを設定
(let ((count 1) (result 1)) ;初期条件
(call/cc
(lambda (body)
;; 継続1。bodyはここのcall/ccの「外側」をクロージャとして表現したもの。
;; 具体的には上部のletから末尾までが範囲となっている。
;; (正確にはλ式。つまり、letの束縛された数値は捕捉対象外である。)
;; tagはそのまた外側なので継続1には捕捉されない。
(set! tag body))) ;そのbodyをtagに代入している。
(call/cc
(lambda (return)
;; 継続2。ここの継続は継続1内に捕捉されている事に注意。
;; 継続1の「外側」に存在しているからである。
;; 条件を満たした場合、継続2はresultを持って計算を脱出する。
(and (= count n) (return result))
(set! count (+ 1 count)) ;手続的計算
(set! result (+ count result))
;; tagへ飛ぶ。tagは事実上一引数関数になってるのでダミー引数'()を渡してる。
(tag '()))))))