今回は文字と文字列、そして UTF-8 を取り上げます。
numb-lambda は Java で書いてあります。したがって、文字、文字列に関しては UNICODE サポートがされています。これから、中間コードコンパイラを書いていくわけですが、実行環境(仮想マシン)は、C で書くことを想定しています。実行環境は REPL を備えています。そうすると、文字列の操作(走査)が必要になってきます。しかし、Java の機能は使えません。Java で書いてある現行の numb-lambda も使えません。これから、書く新しい numb-lambda は、Java の実行環境を必要としない代わりに、Java の機能は使えなくなってしまいます。*1 つまり、UNICODE を numb-lambda 自身でハンドリングしなければならないのです。新しい処理系では入出力にUTF-8のみをサポートします。
早速、numb-lambda を起動して次の例を試してみてください。
> #\南
#\南
> #\無
#\無
> #\λ
#\λ
> (char->integer #\λ)
955
> (integer->char 955)
#\λ
> (char=? #\λ #\λ)
#t
> (char=? #\南 #\無)
#f
> (char>? #\南 #\無)
#f
> (char->integer #\南)
21335
> (char->integer #\無)
28961
> (char->integer #\space)
32
> (char->integer #\newline)
10
>
#\
に続けて一文字を書きます。全角文字や半角文字の区別はありません。その他、#\space
が空白を、#\newline
が改行を表します。char->integer
関数は文字に対する文字コードを返す関数です。反対に integer->char
関数は文字コードに対応する文字を返します。その他、前回(オブジェクト指向?)、で紹介しました述語、char=?、char>?
の利用例を示しています。文字コードは UNICODE となっているので基数を16に変換する関数を書いてみましょう。
(define change-radix
(lambda (x radix)
(let loop ((x x)(acc '()))
(if (zero? x)
(if (null? acc) (cons 0 acc) acc)
(loop (quotient x radix)
(cons (remainder x radix) acc))))))
> (change-radix (char->integer #\λ) 16)
(3 11 11)
>
(define list->string
(lambda (lst tab)
(let ((n (length lst)))
(let ((s (make-string n #\0)))
(let loop ((lst lst)(i 0))
(if (null? lst)
s
(begin
(string-set! s i (string-ref tab (car lst)))
(loop (cdr lst) (+ i 1)))))))))
(define hex-list->string
(lambda (lst m)
(let ((s (string-append
"00000000"
(list->string lst "0123456789abcdef"))))
(string-append
"#x"
(let ((n (string-length s)))
(substring s (- n m) n))))))
> (hex-list->string (change-radix (char->integer #\λ) 16) 8)
"#x000003bb"
> (begin
(display (hex-list->string
(change-radix (char->integer #\λ) 16) 8))
(newline))
#x000003bb
>
以上のように関数を組み合わせることによって、汎用から専用にデータを加工していくことができます。これが、関数型プログラミングの手法です。ただし、若干の副作用は用いています。
string-set!
などが、その代表です。このとおり、Scheme の文字列は mutable (書き変え可能)なのです。新しく出て来た式を一通り見直してみましょう。
(zero? x)
x が 0
かどうかを判定し、#t
か #f
を返します。(quotient x radix)
は x を radix で除算します。小数点以下は切り捨てられます。(remainder x radix)
x が被除数、radix が除数であるときの剰余を返します。(length lst)
リスト lst の長さを返します。(make-string n #\0)
n 文字分の文字列を作ります。#\0 はオプションで、この文字で文字列全体を埋めます。(string-set! s i c)
文字列 s の i 番目(i=0..n-1; n は文字列の長さ)の文字を c に書き変えます。(string-ref s i)
文字列 の i 番目(i=0..n-1 ; n は文字列の長さ)の文字を返します。(string-append s t u v w...)
文字列 s, t, u, v, w... を連結した文字列を返します。文字列は新しく作られたもので、s, t, u, v, w... は破壊されません。(string-length s)
文字列 s の文字数を返します。(substring s m n)
文字列 s の m 番目から n 番目までの部分文字列を返します。文字列全部は、(substring s 0 (string-length s))
に相当します。つまり、m も n も 0 から数えます。ここで、change-radix について残念な問題があります。
> (change-radix #xffffffff 16)
(-1)
> (change-radix #x80000000 16)
(-8 0 0 0 0 0 0 0)
>
int
型を用いています。これは、32ビットの符号付き整数(2の補数表現)です。したがって、MSB が立っていると負数となり、結果のリストも期待したものになりません。これを list->string
に入力するとエラーとなってしまいます。change-radix
の第一引数は、0~#x7fffffff までであれば期待通り機能します。そこで、numb-lambda では、Scheme を拡張しビット毎の演算とシフトができるような関数を追加しています。以下は整数に対して適用可能な関数です。*2
bits-or
ビット毎の論理和bits-and
ビット毎の論理積bits-xor
ビット毎の排他的論理和bits-not
ビットの反転bits-shift-left
左シフトbits-shift-right
右シフトbits-shift-right
算術右シフトchange-radix
ほど、汎用的なものは実現できませんが、2の累乗を基数とする数にならば対応可能です。今回は 16 を基数とする関数だけ書きましょう。
(define dec->hex-list
(lambda (x)
(let loop ((x x) (acc '()))
(if (zero? x)
(if (null? acc) (cons 0 acc) acc)
(loop (bits-shift-right x 4)
(cons (bits-and x #xf) acc))))))
> (hex-list->string
(dec->hex-list (char->integer #\λ)) 8)
"#x000003bb"
> (begin
(display (hex-list->string
(dec->hex-list (char->integer #\λ) 16) 8))
(newline))
#x000003bb
>
基本多言語面だけ
動作すれば良いでしょう。#\λ
の文字コードは、#x000003bb でしたから、UTF-8 では 2 バイトになります。また、#\南
と #\無
は各 3 バイトです。
(define integer->utf-8
(lambda (c)
(cond
((and (>= c 0) (<= c #x7f)) (list c))
((and (>= c #x80) (<= c #x7ff))
(list (bits-or #xc0 (bits-shift-right c 6))
(bits-or #x80 (bits-and c #x3f))))
((and (>= c #x800) (<= c #xffff))
(list (bits-or #xe0 (bits-shift-right c 12))
(bits-or #x80 (bits-and
(bits-shift-right c 6) #x3f))
(bits-or #x80 (bits-and c #x3f))))
(else '()))))
(define integer->hex-string
(lambda (c m)
(hex-list->string (dec->hex-list c) m)))
> (map (lambda (x) (integer->hex-string x 2))
(integer->utf-8 (char->integer #\南)))
("#xe5" "#x8d" "#x97")
> (map (lambda (x) (integer->hex-string x 2))
(integer->utf-8 (char->integer #\無)))
("#xe7" "#x84" "#xa1")
> (map (lambda (x) (integer->hex-string x 2))
(integer->utf-8 (char->integer #\λ)))
("#xce" "#xbb")
>
(define string->utf-8-list
(lambda (s)
(let ((n (string-length s)))
(let loop ((i 0)(acc '()))
      (if (>= i n)
      (reverse acc)
      (loop (+ i 1)
      (cons (map
      (lambda (x)
      (integer->hex-string x 2))
      (integer->utf-8
      (char->integer
      (string-ref s i)))) acc)))))))
> (string->utf-8-list "南無λ")
(("#xe5" "#x8d" "#x97") ("#xe7" "#x84" "#xa1") ("#xce" "#xbb"))
>
> (map sqrt '(0 1 2 3 4 5 6 7 8))
(0.0 1.0 1.4142135623730951 1.7320508075688772 2.0 2.23606797749979 2.449489742783178 2.6457513110645907 2.8284271247461903)
> (map (lambda (x) (expt 2 x)) '(0 1 2 3 4 5 6 7 8))
(1.0 2.0 4.0 8.0 16.0 32.0 64.0 128.0 256.0)
>
今回は以上です。次回は
integer->utf-8
を 1 文字が 6 バイトになるケースまでサポートします。これにより、U+0000 .. U+7FFFFFFF のコードを全て UTF-8 に変換できるようになります。また、この逆変換を行う関数を書きます。余裕があれば、numb-lambda を拡張して、以上を入出力可能にします。*1 Java 版の新しい numb-lambda の実装も検討しています。完成した暁には晴れて Java 版 numb-lambda の全ソースが公開できます。
*2 通常の算術演算を組み合せても同様の関数を書くことが出来ると思われますが、それでは低速過ぎます。
*3 今のところ実装が不十分で仕様を満たしていません。