文字列補間

Gauche は R5RS 処理系だがスクリプト言語的な拡張が多数ある。 文字列補間はそのひとつだ。 文字列の中に書いた式を文字列に埋込んでくれる機能で、例えばこんな風に使う。

(display #`"This is Gauche, version ,(gauche-version).")

実際には #` という記法はリーダによって string-interpolate に置き換えられるので、以下のように書いたのと同じことになる。

(display (string-interpolate "This is Gauche, version ,(gauche-version)."))

string-interpolate はマクロであるので、更に展開されてこうなる。

(display
 (string-append "This is Gauche, version " (x->string (gauche-version)) "."))

要するに文字列を連結するだけの処理に置き換えているだけなのだが、地味に便利なので他の処理系でも使いたいと思って R6RS で書いてみた。

#!r6rs
(library (interpolate)
  (export string-interpolate)
  (import (rnrs))

  (define (x->string str)
    (call-with-values open-string-output-port
      (lambda(out get)
        (write str out)
        (get))))

  (define-syntax string-interpolate
    (lambda (stx)

      (define (string-empty? str)
        (string=? "" str))

      (define (next-token break-char in)
        (call-with-values open-string-output-port
          (lambda(out get)
            (do ((ch (peek-char in) (peek-char in)))
                ((or (eof-object? ch) (char=? ch break-char)) (get))
              (put-char out (read-char in))))))

      (define (parse-string in k)
        (call-with-values open-string-output-port
          (lambda(out get)
            (let ((ch (peek-char in)))
              (cond ((eof-object? ch) ch)
                    ((char=? ch #\,) (read-char in) (parse-datum in k))
                    (else (next-token #\, in)))))))

      (define (parse-identifier in k)
        (read-char in)
        (call-with-values open-string-output-port
          (lambda(out get)
            (let ((ident (next-token #\| in)))
              (read-char in)
              `(,(datum->syntax #'string-interpolate 'x->string)
                ,(datum->syntax k (string->symbol ident)))))))

      (define (parse-list in k)
        (let ((lst (read in)))
          `(,(datum->syntax #'string-interpolate 'x->string)
            ,(datum->syntax k lst))))

      (define (parse-datum in k)
        (case (peek-char in)
          ((#\|) (parse-identifier in k))
          ((#\() (parse-list in k))
          (else ",")))

      (syntax-case stx ()
        ((k str)
         (string? (syntax->datum #'str))
         (cons (datum->syntax #'string-interpolate 'string-append)
               (let ((in (open-string-input-port (syntax->datum #'str))))
                 (let loop ((token (parse-string in #'k)))
                   (if (eof-object? token)
                       '()
                       (cons token
                             (loop (parse-string in #'k))))))))
        ((_ x ...)
         (syntax-violation
          'string-interpolate
          "malformed string-interpolate"
          stx)))))
  )

使用例はこんな感じ。

#!r6rs
(import (rnrs) (interpolate))

(display
 (let ((a "AAA")
       (b "BBB"))
   (string-interpolate "a is ,|a|.\nb is ,|b|.")))

Gauche と違って変数名を囲む縦棒が必須なので注意されたい。

string-interpolate という綴りが長くて面倒だという場合は、あまり筋のよくない方法ではあるがこんなのもある。

#!r6rs
(import (except (rnrs) quasisyntax) (interpolate))

(define-syntax quasisyntax (identifier-syntax string-interpolate))

(display
 (let ((a "AAA")
       (b "BBB"))
   #`"a is ,|a|.\nb is ,|b|."))