クールな文字列連結のテクニック

HTML や RSS など、テキストをプログラムで生成する時に、最終的にどうしても文字列の連結ということをしますよね。

でも Lisp の場合、文字列として組み立てる前に、S 式で (ツリー構造として) ドキュメントを作成するのが普通です。

そのようなツリー構造で書かれたテキストを文字列化するエレガントな方法が、ある ML での Oleg 氏のポストで紹介されていました。

次のような関数です (少しアレンジしてあります):

;; Adapted from: http://srfi.schemers.org/srfi-13/mail-archive/msg00073.html
(define SRV:send-reply
  (opt-lambda ((out (current-output-port)))
    (lambda fragments
      (let loop ((fragments fragments))
        (cond ((null? fragments) out)
              ((not (car fragments))
               (loop (cdr fragments)))
              ((null? (car fragments))
               (loop (cdr fragments)))
              ((pair? (car fragments))
               (loop (car fragments))
               (loop (cdr fragments)))
              ((procedure? (car fragments))
               ((car fragments) (SRV:send-reply out))
               (loop (cdr fragments)))
              (else
               (display (car fragments) out)
               (loop (cdr fragments))))))))

オリジナルとの変更点は、出力ポートを引数として受け取ることと、関数を戻り値として返すことなどです。

実は文字列の連結を一切していない、というところがミソです。下の方の else 節でポートに対し出力することで、連結の過程を省略して一続きの文字列としてプリントしてしまうわけです。

また、string-append を使う場合、引数は全て文字列でなければいけませんが、これだと単純に display するだけなので、文字列やバイト列、数値などが混在していても大丈夫なんです。

利用例:

((SRV:send-reply ftp-port)
 (html:html
   (html:head
     ...)
   (html:body
     (lambda (p)
       (for-each (lambda (url)
                   (p (html:a (@ (href url)))))
                 list-of-urls)))))

html: という接頭辞が付いているのは

("<head>" (list of other elements) "</head>")

のような、ツリー (またはリスト) 構造を返すマクロです。

この例では、ツリーの要素として関数を置いた場合の用法を示しています。p として渡って来ているのは、一行目の (SRV:send-reply ftp-port) を評価した値と同じものです。

長大な URL リストなどがある場合に、それをマッピング処理で一気に要素のリストに展開してしまうのが躊躇われることがあります。場合によってはデータを外部から逐次的に取得しつつ出力したいこともあります。

そんな時、このように処理を遅延したい部分に関数を差し込んで、少しずつ出力していくことが出来るわけです。


さて、この関数をベースにして、次のような関数を作ってみました:

(define (build-bytes . l)
  (get-output-bytes
   ((SRV:send-reply (open-output-bytes)) l)))

ファイルやネットワークのポートではなく、文字列やバイト列のポートに出力することで、まさに「連結」が行えるわけです。

例として、どこかのウェブ・サービスにログインするコードを示します:

;; PLT Scheme
(require (lib "url.ss" "net")
         (lib "26.ss" "srfi"))

(define (post-form action params k)
  (define boundary (format "---------------------------~a"
                           (current-seconds)))
  (define newline "\r\n")
  (define (field key value)
    (list "--" boundary newline
          (format "Content-Disposition: form-data; name=\"~a\""
                  key)
          newline newline value newline))
  (let ((in (post-impure-port
              (string->url action)
              (build-bytes (map (cut apply field <>)
                                params)
                           "--" boundary "--" newline)
              (list
                (format
                  "Content-Type: multipart/form-data; boundary=~a"
                  boundary)))))
    (dynamic-wind
      void
      (cut k in)
      (cut close-input-port in))))

(post-form "http://www.xxx.yy/login"
           '((user "foo") (pass "bar"))
           (lambda (in)
             (get-login-code in)))

post-impure-port は第2引数としてバイト列のデータを取る関数です。が、あくまでもリスト操作関数によって柔軟に部品を組み立てることに専念できるので、非常に楽です。