Hatena::Groupcadr

わだばLisperになる このページをアンテナに追加 RSSフィード

2004 | 12 |
2005 | 01 | 02 | 07 | 10 | 11 |
2006 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2008 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 11 |

2011-06-29

逆もまたしかり

| 21:35 | 逆もまたしかり - わだばLisperになる を含むブックマーク はてなブックマーク - 逆もまたしかり - わだばLisperになる

クヌース先生の「goto文を用いた構造的プログラミング」を読んでいて末尾呼び出しはgotoに直せ、また逆も正しい、との記述を読んで、そういえばあまり逆方向のことは考えないなと思ったので試してみました。

単に試してみただけで特にオチはありません…。

試してみる

(loop :for i :from 0 :below 100 :count i)

マクロの展開系を関数にしたようなgogo

(defun gogo ()
  (declare (optimize (safety 0) (speed 3)))
  (block nil
    (let ((i 0))
      (declare (type (and real number) i))
      (let ((loop-sum 0))
        (declare (type fixnum loop-sum))
        (tagbody
         NEXT-LOOP
           (when (>= i '100)
             (go END-LOOP))
           (when i
             (setq loop-sum (1+ loop-sum)))
           (setq i (1+ i))
           (go NEXT-LOOP)
         END-LOOP
           (return-from nil loop-sum))))))

(gogo)
;=> 100

disassembleしてみる

; disassembly for GOGO
;        XOR ECX, ECX ; no-arg-parsing entry point
;        XOR EDX, EDX
;        JMP L1
; L0:    ADD RDX, 8
;        ADD RCX, 8
; L1:    CMP RCX, 800
;        JL L0
;        MOV RSP, RBP
;        CLC
;        POP RBP
;        RET

上のgogo(loopマクロの展開形)をそのまま再帰呼び出しに変換したようなrecrec

(defun recrec ()
  (declare (optimize (safety 0) (speed 3)))
  (block nil
    (let ((i 0))
      (declare (type (and real number) i))
      (let ((loop-sum 0))
        (declare (type fixnum loop-sum))
        (labels ((NEXT-LOOP ()
                   (when (>= i '100)
                     (END-LOOP))
                   (when i
                     (setq loop-sum (1+ loop-sum)))
                   (setq i (1+ i))
                   (NEXT-LOOP))
                 (END-LOOP ()
                   (return-from nil loop-sum)))
          (NEXT-LOOP))))))

(recrec)
;=> 100

disassembleしてみる

; disassembly for RECREC
;       XOR ECX, ECX ; no-arg-parsing entry point
;       XOR EDX, EDX
;       JMP L2
; L0:   CMP RCX, 800
;       JL L1
;       MOV RSP, RBP
;       CLC
;       POP RBP
;       RET
; L1:   ADD RDX, 8
;       ADD RCX, 8
; L2:   JMP L0

ジャンプの順番と回数が多少違いますが、gogoもrecrecも大体同じものになりました。(まあ、最適化してるからなんですけど…)

めでたしめでたし。

2011-06-23

CLでSRFI-17

| 21:42 | CLでSRFI-17 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-17 - わだばLisperになる

悶々とSRFI実装をしていますが、今回は、SRFI-17 一般化されたset! です。

Schemeのset!は、CLでいうsetqで、setfではないのですが、setf的に読み出してきた場所に書き込むという一般化された代入を実現するのがSRFI-17です。

動作

(use-package :srfi-17)

(defun (setter car) (list val)
  (rplaca list val))

(let ((list (list 1 2 3 4)))
  (set! (car list) :foo)
  list)
;=> (:FOO 2 3 4)

(let ((list (list 1 2 3 4)))
  (funcall #'(setter car) list :foo)
  list)
;=> (:FOO 2 3 4)

移植について

set!の方は単純にsetfへのエイリアスとすればOKですが、setterはどうしたもんかと悩みます。

(setter foo)という形式は諦めれば良いのですが、処理系依存の(SBCL)の定義を追加して対処してみることにしました。(define-function-name-syntaxで定義可能)

といっても、((setter car) list :car)まで動くようにするのは大変なので(funcall #'(setter car) list :car)程度で妥協。

2011-06-20

pkg-bind再び

| 18:28 | pkg-bind再び - わだばLisperになる を含むブックマーク はてなブックマーク - pkg-bind再び - わだばLisperになる

以前Zetalispのpkg-bindをCLで再現するのに挑戦したことがありました。

pkg-bindとはどういうものかというと、囲んだ範囲は指定したパッケージ内にin-packageしたような感じに書けるというものです。

(pkg-bind :drakma
  (let ((fun #'http-request))
    (funcall fun "http://example.com")))

これが、

(LET ((DRAKMA::FUN #'DRAKMA:HTTP-REQUEST))
  (FUNCALL DRAKMA::FUN "http://example.com"))

こんな感じに解釈されます。

前回は中途半端な感じでしたが、通勤途中に前回のアプローチを一捻りする方法を思い付いたのでメモ。

前回は、パッケージ名を含んだ文字列を作成して、それを元にS式を組み立てましたが、今回は、文字列の作成に、PRINT-OBJECTを使ってみます。

具体的には、シンボルを読んで、あるオブジェクトに変換して、そのプリティプリントが、#.(CL:INTERN "FOO" "CL")という風になるようにします。

あとは、ボディを再帰的に走査して、文字列として出力し、READ-FROM-STRINGし、それをDEFMACROのボディとします。

(defclass intern-form ()
  ((name :initarg :name)
   (package :initarg :package)))

(defmethod print-object ((obj intern-form) stream)
  (format stream
          "#.(CL:INTERN ~S ~S)"
          (slot-value obj 'name)
          (slot-value obj 'package)))

(defun up-symbol (elt pkg)
  (typecase elt
    (symbol
       (let ((name (string elt)))
         (make-instance 'intern-form
                  :name name
               :package (package-name
                         (let ((elt-pkg (symbol-package elt)))
                           (cond ((eq elt-pkg (find-package pkg))
                                  pkg)
                                 ;;
                                 ((and (eq elt-pkg (find-package *package*))
                                       (find-symbol (string elt) pkg))
                                  pkg)
                                 ;;
                                 ('T elt-pkg)))))))
    ;;
    (otherwise elt)))

(defun symbol-to-intern-form (tree pkg)
  (cond ((null tree)
         tree)
        ;;
        ((atom (car tree))
         (let ((elt (car tree)))
           (cons (if (eq 'pkg-bind elt)
                     'pkg-bind
                     (up-symbol elt pkg))
                 (symbol-to-intern-form (cdr tree) pkg))))
        ;;
        ('T (cons (symbol-to-intern-form (car tree) pkg)
                  (symbol-to-intern-form (cdr tree) pkg)))))

(defmacro pkg-bind (pkg &body body)
  `(progn
     ,@(read-from-string
        (write-to-string
         (symbol-to-intern-form body (package-name pkg))))))

pkg-bindはそれほど使う機会もありませんが、他のパッケージからコピペしたコードをとりあえず手元のパッケージ内で動作確認したい場合などにそれなりに便利に使えます。

2011-06-19

CLでSRFI-9

| 16:32 | CLでSRFI-9 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-9 - わだばLisperになる

適当にSRFI実装をしていますが、今回は、SRFI-9 レコード型の定義 です。

CLでいう構造体かクラスですが、SRFI-9の define-record-typeも定義時にアクセサやモディファイア等を一式作成するものです。

動作

define-record-type の使い方ですが、定義は、

(define-record-type レコード名 コンストラクタ 述語 *(スロット アクセサ名 モディファイア名))

という風に記述します。

;; pairの定義
(define-record-type pair
  (kons x y)
  pair?
  (x kar set-kar!)
  (y kdr set-kdr!))

;; レコードの作成
(defvar kons (kons 1 2))

(pair? kons)
;=> T

(kar kons)
;=> 1

(kdr kons)
;=> 2

(set-kar! kons 100)
;=> 100

kons
;=> #S(PAIR :KAR 100 :KDR 2)

(set-kdr! kons 200)
;=> 200

kons
;=> #S(PAIR :KAR 100 :KDR 200)

上記のpairの定義は、下記のように展開されることにしてみました。

(progn
  (defstruct (pair
               (:constructor kons (x y &aux (kar x) (kdr y)))
               (:predicate pair?)
               (:conc-name ""))
    kar kdr)
  (srfi-9-internal::define-modifier kar set-kar!)
  (srfi-9-internal::define-modifier kdr set-kdr!))

仮引数とスロット名が対応していませんが、アクセサをdefstruct標準の機能で作成しているのでslot名=アクセサ名となっています。

コンストラクタの引数はdefstructにそのまま渡しているので、defstructのラムダリストが使えます。(これはboa lambda listと呼ばれるようです)

(define-record-type foo
  (mkfoo a b &key c)
  foo?
  (a fooa)
  (b foob)
  (c fooc))

(defvar foo (mkfoo 1 2 :c 3))
;=> FOO

foo
;=> #S(FOO :FOOA 1 :FOOB 2 :FOOC 3)

(incf (fooc foo) 100)
;=> 103

foo
;=> #S(FOO :FOOA 1 :FOOB 2 :FOOC 103)

defstructはオプションが豊富で魔窟な感じですが、こういうシンプルに使えるマクロを用意して使ってみるのもありかなと思います。

2011-06-17

CLでSRFI-4

| 22:31 | CLでSRFI-4 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-4 - わだばLisperになる

必死に願掛けSRFI実装をしていますが、今回は、SRFI-4 一様数値ベクタ型 です。

通常のベクターは要素の型として任意のオブジェクトが取れるのですが、同じ型の数値しか入らないことが予め分かっていたりする場合には、それにフィットした効率の良いベクタを使いたい、というのが、このSRFIの目的のようです。

動作

(u8vector 0)
;=> #U8(0)

#u64(0)
;=> #u64(0)

(make-s8vector 8)
;=> #S8(0 0 0 0 0 0 0 0)

(u8vector? #u8(1 2 3))
;=> T

移植について

#u8(0)等の読み込みには、リーダーマクロを定義することになりますが、#sが構造体で使われているので競合を回避。

また、#U(0 0 0 0 ...)のような表示をするには、プリンタも変更する必要があります。残念ながらprint-objectで定義可能な部分ではないようなので処理系の関数を上書きすることになります。

CLだと、#u16(0 0 0)もu8(0 0 0)も#(0 0 0)と表示されるので区別がつかないんですねー。もちろんオブジェクトはちゃんと型どおりではあるのですが。

2011-06-13

CLでSRFI-7

| 21:46 | CLでSRFI-7 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-7 - わだばLisperになる

ちまちまとSRFIをCLに移植していますが、今回は、SRFI-7 フィーチャーベースの設定言語 です。

フィーチャーというのは、処理系がサポートしていて読み込める/読み込んでいる、ある機能のことのようですが、SRFI-7はそのフィーチャーの情報を元に、ライブラリを読み込んだり設定するための設定言語とのこと。

Schemeとは全く別の種類の言語、ということですが、混ぜて考えてはいけないことが何かあるのでしょう。

(program...)の中に、requires、files、code、feature-cond等が記述できて、これを組み合わせてライブラリを読み込みます。

requiresは必要とするフィーチャー、filesはロードするファイル名の指定、codeは、設定時に実行するコード、featture-condは、フィーチャーによって実行するものを切り分けるためのcondのようです。

手軽に書けるので、こういうのも悪くないかなと思ったり。

ついでなので、ASDFのSYSTEMを読み込めるように機能を追加してみました。

動作

(program (requires :sbcl))
;=> NIL

(program (files "/tmp/foo.lisp"
                "/tmp/bar.lisp"
                "/tmp/baz.lisp"))
;=> NIL

(program (feature-cond (:sbcl)))
;=> NIL

(program (feature-cond (:sbcl (requires :srfi-1))
                       (:srfi-0 (requires :sbcl))))
;=> NIL

(program
 (feature-cond (:allegro (systems :srfi-0 :srfi-1))
               (:sbcl (systems :srfi-2))))

(program (requires :sbcl)
         (code (print :sbcl))
         (files "/tmp/foo.lisp")
         (feature-cond (:x86-64)))
;->
;   :SBCL
;=> NIL

2011-06-12

CLでSRFI-6

| 16:17 | CLでSRFI-6 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-6 - わだばLisperになる

ぼーっとSRFIをCLに移植していますが、今回は、SRFI-6 基本的な文字列ポート です。

CLに同じ機能のmake-string-input-stream等があるので、それのエイリアスを定義しただけで終了。

2011-06-11

CLでSRFI-78

| 22:35 | CLでSRFI-78 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-78 - わだばLisperになる

ぼんやりとSRFIをCLに移植していますが、今回は、SRFI-78 軽量テスト です。

CLにもユニットテストは色々ありますが、SRFIみたいなものである程度標準化されてるというのは良いですね

基本的な使い方は、 checkの中で、式と期待する値を=>を挟んで書く、というシンプルなものです。

例のごとくシンボルの扱いが面倒臭いので、=> ではなく:=>としてみています。

便利機能としては、SRFI-42と組合さったcheck-ecというものが用意されています。

(use-package :srfi-78)

(check (+ 2 2) :=> 4)
;->
;   (+ 2 2) :=> 4 ; correct
;
;=>

(check (+ 2 2) :=> 5)
;->
;   (+ 2 2) :=> 4 ; *** failed ***
;    ; expected result: 5
;
;=>

(check-ec (:range e 100)
          (:let x e)
          (and (< e 50) (< x 50)) :=> t (e x))
;->
;   (LET ((E 50) (X 50))
;     (AND (< E 50) (< X 50))) :=> NIL ; *** failed ***
;    ; expected result: T
;

2011-06-10

CLでSRFI-48

| 19:47 | CLでSRFI-48 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-48 - わだばLisperになる

だらだらとSRFIをCLに移植していますが、今回は、SRFI-48 中級の書式文字列です。

SRFI-28にもformatがありましたが、48は機能が増えて中級ということです。CLのformatに比べたら、まだまだってことなんでしょうか。

SRFI-42や、86みたいな例もあることですし、今後のSRFIにとんでもない究極のformatが出てくることを期待しています。

しかし、SRFIって意外と似たものが複数提案される傾向があるんですね。

大体CLのformatに似た感じですが、第二引数が文字列だった場合は、文字列が返ってきたりします。

ちなみに、CLのように文字列指示文字列のところに関数を与えることはできません。

面白いと思ったのは、~Hで、ヘルプが表示されるところ。CLだったらドキュメント文字列にするところだと思いますが、Schemeにそういうものはないので一工夫ということなのでしょう。

(srfi-48:format "~H")
;=> "(format [<port>] <format-string> [<arg>...]) -- <port> is T, nil or an output-port
;   OPTION  [MNEMONIC]      DESCRIPTION     -- Implementation Assumes ASCII Text Encoding
;   ~H      [Help]          output this text
;   ~A      [Any]           (display arg) for humans
;   ~S      [Slashified]    (write arg) for parsers
;   ~W      [WriteCircular] like ~s but outputs circular and recursive data structures
;   ~~      [tilde]         output a tilde
;   ~T      [Tab]           output a tab character
;   ~%      [Newline]       output a newline character
;   ~&      [Freshline]     output a newline character if the previous output was not a newline
;   ~D      [Decimal]       the arg is a number which is output in decimal radix
;   ~X      [heXadecimal]   the arg is a number which is output in hexdecimal radix
;   ~O      [Octal]         the arg is a number which is output in octal radix
;   ~B      [Binary]        the arg is a number which is output in binary radix
;   ~w,dF   [Fixed]         the arg is a string or number which has width w and d digits after the decimal
;   ~C      [Character]     charater arg is output by write-char
;   ~_      [Space]         a single space character is output
;   ~Y      [Yuppify]       the list arg is pretty-printed to the output
;   ~?      [Indirection]   recursive format: next 2 args are format-string and list of arguments
;   ~K      [Indirection]   same as ~?
;   "

移植について

参照実装が、一関数で300行越えの塊というところに若干めげそうになりましたが、名前空間の汚染を嫌ったかなにかで一つに纏まっているだけのようなので分解して定義しました。

また、ソースがぶらさがり括弧派のコードで、Schemeにもそういう人はいるんだなと妙に感心。

2011-06-08

CLでSRFI-10

| 19:47 | CLでSRFI-10 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-10 - わだばLisperになる

きまぐれでCLに移植しているSRFIですが今回は、#,外部フォームです。

#,はリーダーマクロですが、CLの#.(リード時EVAL)に似た感じのものです。

違いとしては、CLの#.は後続の式をEVALしますが、SRFI-10の#,はAPPLYします。

ちなみに、#,はCLtL1やZetalispでは、ロード時評価のリーダーマクロでしたがANSI CLでは、load-time-valueに置き換えられて廃止になっています。

下準備

(srfi-10:enable-read-time-application)
(import 'srfi-10:define-reader-ctor)
;; CLの #.
#.(+ 3 (+ (+ 3 3) 3))
;=> 12

;; SRFI-10の#,
(define-reader-ctor '+ #'+)

#,(+ 3 #,(+ #,(+ 3 3) 3))
;=> 12

そのままでは、式のCDRが再帰的に評価されていかないので#,をネストしたりする必要があります。

単純なリード時の評価以外の主な応用としては、構造体などの簡便な表記などがあるようです。

(defstruct foo x y z)

(define-reader-ctor 'foo #'make-foo)

#,(foo :x 1 :z 2 :y 3)
;=> #S(FOO :X 1 :Y 3 :Z 2)

(defstruct bar x y z)

(define-reader-ctor 'bar
  (lambda (x y z) (make-bar :x x :y y :z z)))

#,(bar 1 2 3)
;=> #S(BAR :X 1 :Y 2 :Z 3)

クラスにreader-ctorを定義して、さらにprint-objectも設定してみる

(defclass baz ()
  ((x :initarg :x)
   (y :initarg :y)
   (z :initarg :z)))

(define-reader-ctor 'baz
  (lambda (&rest args)
    (apply #'make-instance 'baz args)))

(defmethod print-object ((obj baz) stream)
  (with-slots (x y z) obj
    (format stream "#,(BAZ :x ~S :y ~S :z ~S)" x y z)))

#,(baz :x 1 :y 2 :z 3)
;=> #,(BAZ :x 1 :y 2 :z 3)

移植について

SRFI-10の文献だけだと微妙な挙動が分からなかったためGaucheで動作を確認して大体のところはGaucheと同じような挙動にしました。

関数のルックアップは、シンボルのSYMBOL-FUNCTIONを見るようにもしてみましたが、これもGaucheの動作に合せてdefine-reader-ctorで定義したものだけ引いてくるようにしてみました。

ちなみにctorはconstructorの略のようで、調べてみたらちらほら用例があるようです。catみたいな略し方ですね。

2011-06-06

CLでSRFI-3

| 19:18 | CLでSRFI-3 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-3 - わだばLisperになる

はてSRFI-3なんか聞いたことないな、という方も多いかもしれませんが、SRFIには没になったものもあり、その番号は欠番となってしまいます。

SRFI-3は記念すべき一番乗りの没SRFIです。

没になったものを移植してどうするんだ、という気もしますが、移植すること自体が目的なのでとりあえず移植。

SRFI-3の内容ですが、セットとしてのリストを扱うライブラリになります。

どうやら、SRFI-1のリストライブラリに吸収合併される形となったため、単独のSRFIとしては成立しなかった模様。

(srfi-1:lset-union #'= '(1 2 3 4) '(1 2 3 4))
;=> (1 2 3 4)

(srfi-3:lset-union #'= '(1 2 3 4) '(1 2 3 4))
;=> (1 2 3 4)

(srfi-3:union '(1 2 3 4) '(1 2 3 4))
;=> (1 2 3 4)

とはいえSRFI-1ともほんのり微妙に違っているところもあったりします。

移植について

大体SRFI-1の移植と同じですが今回は、もう一歩進めてオリジナルの定義の字面は一切変更しないことを目標に、マクロなどでCLとSchemeの差を埋めてみることにしました。

関数定義が結構あり、#'等をどうごまかすかということになりますが、バリューセルにも関数を詰めることで#'を書かなくても良いようにしました。

かなり強引ですが、ソースを変更しなくて良いのは気持ちが良いですね。S式みな兄弟。

2011-06-04

CLでSRFI-0

| 20:29 | CLでSRFI-0 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-0 - わだばLisperになる

SchemeのRequest For Implementationに意味なくCLで応えている日々ですが、今回は、SRFI-0 フィーチャーベースの条件展開 です。

CLerにはお馴染みの、#+foo #-barという、リードマクロですが、これのScheme版というところです。

CLでは、リード時に作用しますが、SRFI-0では、マクロ展開時に作用します。

(use-package :srfi-0)

(progn
  (cond-expand (:sbcl
                (print 'sbcl?)))
  #+sbcl (print 'sbcl!)
  nil)
;->
;   SBCL?
;   SBCL!
;=> NIL

移植について

CLに移植するにあたり*features*の機能が活かされないのももったいないので*features*内容も参照するようにしてみました。

SRFI-0の参照実装では、パターンマッチのマクロ内で完結するように組まれていてフィーチャーの追加がなんとなく面倒そうに見えたのですが(マクロ内にずらーっと並べて書くんだろうかとか)、Gaucheでは、実行時にフィーチャーが追加できるようにするため伝統マクロで実装されていました。なるほど。

2011-06-02

CLでSRFI-28

| 21:59 | CLでSRFI-28 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-28 - わだばLisperになる

移植というより単なるコピペな感じのSRFI移植ですが、今回は、SRFI-28 基本的な書式文字列です。

CLのFORMATの基本的な機能の~A、~S、~%、~~が使えるのみのシンプルなFORMATというところですが、ストリームには出力されず文字列が返ってきます。

(shadowing-import 'srfi-28:format)
;=> T

(format "foo~abaz~%" 'bar)
;=> "fooBARbaz
;   "

ちなみに、自分はCLのFORMATの書式指示子は大文字で書く派です。

MacLISPの時代から大文字で~Aと書いても小文字で~aと書いてもOKだったようですが、コードサンプルは古えから大文字が多いためか、大文字で書いている例が多いようです。

Schemeは小文字で書く人が多い気がしますが、上のSRFI-28の参照実装では小文字しか見ていないので小文字で書く必要があります。

今度無駄に統計でも取ってみようかなとも思っています。

2011-06-01

CLでSRFI-11

| 19:43 | CLでSRFI-11 - わだばLisperになる を含むブックマーク はてなブックマーク - CLでSRFI-11 - わだばLisperになる

一日1SRFIな感じで進めていますが、今回は、SRFI-11 です。

SRFI-8と同じくSRFI-11も多値の束縛のための構文ですが、こんなに近い番号で似たようなものがあるというのも不思議な気もします。ざっとSRFIを眺めると当然かもしれませんが後に出て来た方が大抵は機能が豊富なようです。

それで、その機能が豊富なところですが、receiveは一つのフォームを束縛するのみでしたが、let-valuesは多段をサポートします。

(use-package :srfi-11)

(let-values (((x y z) 
              (values 1 2 3))
             ((a b c) 
              (values 1 2 3))
             ((d e f) 
              (values 1 2 3)))
  (list x y z a b c d e f))
;=> (1 2 3 1 2 3 1 2 3)

(let*-values (((x y z) 
               (values 1 2 3))
              ((x y z) 
               (values x y z))
              ((x y z) 
               (values x y z)))
  (list x y z))
;=> (1 2 3)

という風にletとlet*のお約束もそのままというところ。

多段にしたため、括弧の入れ子が1段増えてしまいますが、括弧好きな自分でも、3階層位で構造を見失いかけるのでこの辺りが限界かなと感じます。

上のように改行すれば、まだなんとか大丈夫ではありますが。

ちなみにGoogle コード検索で調べてみたところではCLでlet-valuesを自作している人はちらほらいるようです。

多段のMULTIPLE-VALUE-BINDが欲しい場合はたまにあるかもしれないですね。