ラベル 入れ子のかっこ の投稿を表示しています。 すべての投稿を表示
ラベル 入れ子のかっこ の投稿を表示しています。 すべての投稿を表示

2010年9月16日木曜日

入れ子のかっこ

前回のブログの点記法の続きである. とりあえず練習をしよう. 私の手元のPaul Rosenbloom, The Elements of Mathematical Logic (Dover 1950)に点記法の論理式が沢山出てくる. それでテストしてみた. まずSchemeでBoole値が0と1のnot, or, impを定義する.

(define (not p) (- 1 p))
(define (or p q) (quotient (+ p q 2) 3))
(define (imp p q) (or (not p) q))

テストをするには, mapが便利.

(map not '(0 1)) => (1 0)
(map or '(0 0 1 1) (0 1 0 1)) => (0 1 1 1)
(map imp '(0 0 1 1) '(0 1 0 1)) => (1 1 0 1)


上の本では, 沢山並ぶ -> には特にかっこを(点で)示さない. -> はleft associativeであって, (p -> q -> r) は ((p -> q) -> r) なのだ.

ではやってみよう.

I4 p -> .q -> r. -> .p -> q -> .p -> r
かっこに変えると
((p -> (q -> r)) -> ((p -> q) -> (p -> r)))
Schemeの定義は
(define (i4 p q r)
(imp (imp p (imp q r)) (imp (imp p q) (imp p r))))
実行
(map i4 '(0 0 0 0 1 1 1 1) '(0 0 1 1 0 0 1 1)
'(0 1 0 1 0 1 0 1))
=> (1 1 1 1 1 1 1 1)

もう1つ.

T2 q -> r -> .p -> q -> .p -> r
かっこに変えると
((q -> r) -> ((p -> q) -> (p -> r)))
Schemeの定義は
(define (t2 p q r)
(imp (imp q r) (imp (imp p q) (imp p r))))
実行
(map t2 '(0 0 0 0 1 1 1 1) '(0 0 1 1 0 0 1 1)
'(0 1 0 1 0 1 0 1))
=> (1 1 1 1 1 1 1 1)

とうまくいく.

かっこ記法と点記法の変換だが, まずかっこから点へは, すべての2項演算子をかっこでくくることにし, つまり

<primary>==<letter>|(<expression>)
<expression>==<primary>|<expression>*<expression>

だけとする. expressionの例は a, a * b, (a * b) * c, ...など.

expression * expression を点記法にするには, expression' lp * rp expression とする. expression'はexpressionを点記法に変えたものである. lpとrpは, 左右のexpressionをひとまとまりにする点である. lpは左のexpression'で使った最大の右点の数より多く, rpは右のexpressionで使った最大の左点の数より多くなければならない. 従って, 下請けの変換は, expression'を返すと同時に, 自分の使った右点, 左点を返すことにする.

letterの場合は, letterの他に, 右点,左点として, 0,0を返す.

* の場合は, 左の式を変換て置き, その右点+1の左点を置き, 演算子を置き, 右の式を変換し, その左点+1の右点を置き, 右の式を置く. また自分で使った左点, 右点も返す.

(define (dotconv exp)
(display (list exp))
(if (symbol? exp) (list '(0 0) exp)
(let* ((l (dotconv (car exp)))
(r (dotconv (caddr exp)))
(op (cadr exp))
(rlp (+ (cadar l) 1))
(le (cadr l))
(lrp (+ (cadar r) 1))
(re (cadr r)))
(list (list rlp lrp)(list le rlp op lrp re)))))


(dotconv 'a) => ((0 0) a)
(dotconv '(a * b)) => ((1 1) (a 1 * 1 b))
(dotconv '((a * b) * c)) => ((2 1) ((a 1 * 1 b) 2 * 1 c))
(dotconv '((a * b) * (c * d))) =>
((2 2) ((a 1 * 1 b) 2 * 2 (c 1 * 1 d)))

この後は ((a 1 * 1 b) 2 * 2 (c 1 * 1 d)) を a * b . * . c * d にしたい. flattenし, 整数はそれ引く1の点を出力する.

(flatten '((a 1 * 1 b) 2 * 2 (c 1 * 1 d))) => (a 1 * 1 b 2 * 2 c 1 * 1 d)

(define (convstream l)
(apply string-append (apply append
(map (lambda (x)
(list (if (number? x) (make-string (- x 1) #\.)
(symbol->string x)) " "))
(flatten l)))))

(convstream '((a 1 * 1 b) 2 * 2 (c 1 * 1 d)))

(define (par->dot exp) (convstream (cdr (dotconv exp))))

(par->dot 'a) => "a "
(par->dot '(a * b)) => "a * b "
(par->dot '((a * b) * c)) => "a * b . * c "
(par->dot '((a * b) * (c * d))) => "a * b . * . c * d "


一方, 私の考えた逆変換はこうだ.


1.(((a * b) * (c * d)) * ((e * f) * (g * h)))
2."a * b . * . c * d .. * .. e * f . * . g * h "

1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
3.(a 0 * 0 b 1 * 1 c 0 * 0 d 2 * 2 e 0 * 0 f 1 * 1 g 0 * 0 h)
4.29
5.((-1 5) (-1 13) (15 21) (23 29) (15 29) (7 13))
6.(< < < a * b > * < c * d > > * < < e * f > * < g * h > > >)
7.(((a * b) * (c * d)) * ((e * f) * (g * h)))


上の例で, 1. は元のかっこ記法の式. 2. はそれを点記法にしたもの. 逆変換はここから始まる. まず各演算子に左点と右点があるものとし, 3のように変換する. その上の2行は, 各要素の位置を示す. 0から28まであるから, lengthをとると, 4. のように, 29.

位置 1,5,9,..のように, 4を法として1の位置は左点. 3の位置は右点である. さらに, 演算子は2(mod 4), 変数は0(mod 4)である.

次に各右点>0にはこの右方にある相棒の左点を探し, また左点>0にはこの左方にある右点を探し, 対にする. 上の例では 5. が対のリストである. この読み方は, 5の位置の左点を越える右点は, 左方にはないので, -1とし, (-1 5)とする. 13の位置の左点も同じ. 21の位置の左点は, 15の位置の右点の方が大きいので, スコープはここまでとなり, (15 21)が出来る. 3. と5. の情報から, 6. を作るのだが, (-1 5)のような対があれば, -1に左かっこ, 5に右かっこを置く. この挿入で, 位置がずれると困るから, 挿入は番号の多い右端から行う. 右かっこと左かっこの区別は, 4を法とした剰余の1か3で決る. まだこの段階は記号列であるが, 通常のかっこを記号として挿入すると, 分かり難いから, 角かっこを使っている. 出来たのは6. である.後は, 入れ子の式の読込みルーチンを書けばよい. それにより, 7. が得られる.

(define (iconv s) ;2. -> 3.
(define (char->symbol c)
(string->symbol (list->string (list c))))
(define (reads n s)
(cond ((null? s) s)
((char=? (car s) #\Space) (reads n (cdr s)))
((char=? (car s) #\.) (reads (+ n 1) (cdr s)))
(else (cons n
(cons (char->symbol (car s)) (reads 0 (cdr s)))))))
(let ((ss (string->list s)))
(cons (char->symbol (car ss)) (reads 0 (cdr ss)))))

(define (makepair s) ;3. -> 5.
(let ((len (length s)) (ps '()))
(do ((i 3 (+ i 4))) ((>= i len))
(let ((r (list-ref s i)) (k len))
(do ((j (- len 4) (- j 4))) ((< j i))
(let ((l (list-ref s j)))
(if (< r l) (set! k j))))
(if (> (- k i) 2) (set! ps (cons (list i k) ps)))))
(do ((i (- len 4) (- i 4))) ((< i 0))
(let ((l (list-ref s i)) (k -1))
(do ((j 3 (+ j 4))) ((> j i))
(let ((r (list-ref s j)))
(if (> r l) (set! k j))))
(if (> (- i k) 2) (set! ps (cons (list k i) ps)))))
ps))

(define (makestr ps s len) ;5. 3. (length 3) -> 6.
(define (insert l n a)
(if (<= n 0) (cons a l)
(cons (car l) (insert (cdr l) (- n 1) a))))
(set! ps (sort (apply append ps) >))
(for-each (lambda (x) (set! s (insert s x
(if (= (modulo x 4) 3) '< '>)))) ps)
(append '(<) (filter symbol? s) '(>)))

(define (str->sexp str) (define i 0) ;6. -> 7.
(define (getch) (let ((ch (list-ref str i)))
(set! i (+ i 1)) ch))
(define (read) (let ((ch (getch)))
(cond ((eq? ch '<) (readtail))
((eq? ch '>) '())
(else ch))))
(define (readtail) (let ((x (read)))
(if (null? x) x (cons x (readtail)))))
(read))

2010年9月5日日曜日

入れ子のかっこ

Lispの好きな人は, かっこだらけのS式を何とも思わない. 反対にhtmlのようなタグでの入れ子には耐えられない.

しかし, S式を声を出して読む時, 「かっこ開く」「かっこ閉じる」や「左かっこ」「右かっこ」というのは煩わしい. 私は昔から,左かっこを「かっこ」, 右かっこを「こっか」と読むことにしている. (Algol 68で, 添字に使う [ と ] を, sub symbol, bus symbolと呼んでいたのをまねした.)

ところで, 今回の話題は, 自然言語の構文解析である. 構文解析木を図で表わすのは容易だが, かっこ付きでも表わせる.

Time flies like an arrow は 光陰矢の如し と構文解析するのが普通だが, 時間の蝿は矢が好きだ という笑い話もある. 前者は Time (flies (like an arrow)), 後者は (Time flies) like an arrow と表現すればよい.

SICPに簡単な自然言語解析の話がある. その問題4.45は, The professor lectures to the student in the class with the cat を5通りに構文解析せよ, だ. やってみると

1 ((the professor)
(((lectures
(to (the student)))
(in (the class)))
(with (the cat))))
2 ((the professor)
((lectures
(to (the student)))
(in ((the class)
(with (the cat))))))
3 ((the professor)
((lectures
(to ((the student)
(in (the class)))))
(with (the cat))))
4 ((the professor)
(lectures
(to (((the student)
(in (the class)))
(with (the cat))))))
5 ((the professor)
(lectures
(to ((the student)
(in ((the class)
(with (the cat))))))))

それぞれの和訳は

1 教授は(猫を連れて(教室で(学生に講義する)))
2 教授は((猫を連れた教室)で(学生に講義する))
3 教授は(猫を連れて((教室の学生)に講義する))
4 教授は((猫を連れた(教室の学生))に講義する)
5 教授は(((猫を連れた教室)の学生)に講義する)

猫を連れているのは, 1と3は教授. 2と5は教室. 4は学生である.

ウェブを眺めていて, H.B.Curryの書いたページを見つけた. ただで見られるのは1ページ目だけだが, 今はそれだけ見られれば充分である.

論理の式には, かっこの代りに点が打ってあるものがある. 件のページには, その提案が書いてある.

Let us suppose that a group of dots on the right of an operation or prefix denotes the beginning of a bracket which extends to the right until it meets a group with an equal or larger number of dots on the left of an operation; and that the scope of a group of dots on the left of an operation shall extend to the left unitl it reaches a larger group of dots on right of some operation.


つまり,
p -> q . -> . q -> r .. -> . p -> r
は
((p -> q) -> (q -> r)) -> (p -> r)
のことである.

この方式を何と言うか知らないが, dot notation つまり 点記法ということにする.

上の和訳の入れ子のかっこ構造から, 点記法を使い, かっこを外したい. それには, 演算子がどれか決めたい. 演算子は左かっこの右, 右かっこの左にあるのだから,

教授は猫を連れた教室の学生に講義する

の赤字の助詞類を演算子と考える. (猫を連れた, 教室の は形容詞句の時で, 副詞句の時は, 猫を連れて, 教室で になる.)

すると, それぞれの例文は

1 教授は . 猫を連れて . 教室で . 学生に講義する
2 教授は .. 猫を連れた教室 . で . 学生に講義する
3 教授は . 猫を連れて .. 教室の学生 . に講義する
4 教授は ... 猫を連れた . 教室の学生 .. に講義する
5 教授は .. 猫を連れた教室 . の学生 . に講義する

となる.

この点を , 2点を ,, に変えると
1 教授は, 猫を連れて, 教室で, 学生に講義する
2 教授は,, 猫を連れた教室, で, 学生に講義する
3 教授は, 猫を連れて,, 教室の学生, に講義する
4 教授は,,, 猫を連れた, 教室の学生,, に講義する
5 教授は,, 猫を連れた教室, の学生, に講義する
のようになる.

点記法に慣れると, これで読めなくもない. では声を出して読むにはどうするか.

Shakespeareの時代, 劇の台本には句読点が何種類かあり, それぞれで間の取り方が違うという話を読んだことがある. そのように, カンマ2つはカンマ1つより長く間をとれば, この例は構文木が分かるのではないかと考えるが, どうだろうか.

2010年2月12日金曜日

入れ子のかっこ

Gosperのハック

定数ステップで次を求めるかっこの足跡のアルゴリズムは, Gosperのハックにヒントがあるらしい. GosperのハックはMITのAIラボのHAKMEMのitem 175にあり, To get the next higher number with the same number of 1 bits のプログラムである. 元はPDP10の機械語だが, TAOCPのex7.1.3--20に通常の書き方がしてある. それをまたSchemeにすると

(define (gosper x)
(let* ((u (iand x (- x)))
(v (+ x u)))
(+ v (irsh (quotient (ixor v x) u) 2))))

iand, ixor, irshは符号なし整数値を二進数とみてビット演算する. 途中結果も含めて計算の進行状況を見るとこうなる.

x u v xor x / u y
000111 000001 001000 001111 001111 001011
001011 000001 001100 000111 000111 001101
001101 000001 001110 000011 000011 001110
001110 000010 010000 011110 001111 010011
010011 000001 010100 000111 000111 010101
010101 000001 010110 000011 000011 010110
010110 000010 011000 001110 000111 011001
011001 000001 011010 000011 000011 011010
011010 000010 011100 000110 000011 011100
011100 000100 100000 111100 001111 100011
100011 000001 100100 000111 000111 100101
100101 000001 100110 000011 000011 100110
100110 000010 101000 001110 000111 101001
101001 000001 101010 000011 000011 101010
101010 000010 101100 000110 000011 101100
101100 000100 110000 011100 000111 110001

これをみて理由を考える. まずxがα01a0bであったとする. ただしa≥1, b≥0である. (1aは1がa個並んでいること.) 上からの4行についていえば
x=000111, α=00, a=3, b=0
x=001011, α=001, a=2, b=0
x=001101, α=0011, a=1, b=0
x=001110, α=0, a=3, b=1
である. uは右端の1だから u=10b. vはxの右端の1に1を足すから, a桁連続している1が0になり, 繰上げが出る. v=α10a+b. xとvをxorするとαがキャンセルされて, 1a+10bになる. uで割ると0bがなくなり, 2ビット右シフトすると, 1a+1が1a-1になり, 結局 α10b+11a-1が得られる.
xのαの右にあった0は1に変り, bが1増え, aが1減り, 場所が交代した.
結果的に1の数は変らない.

Gosparハックの逆関数もある.(TAOCPex7.1.3--21)


(define (igosper y)
(let* ((t (+ y 1)) (u (ixor t y)) (v (iand t y)))
(- v (quotient (iand v (- v)) (+ u 1)))))


次にように計算が進行する.

y t u v and -v u+1 x
111000 111001 000001 111000 001000 000010 110100
110100 110101 000001 110100 000100 000010 110010
110010 110011 000001 110010 000010 000010 110001
110001 110010 000011 110000 010000 000100 101100
101100 101101 000001 101100 000100 000010 101010
101010 101011 000001 101010 000010 000010 101001
101001 101010 000011 101000 001000 000100 100110
100110 100111 000001 100110 000010 000010 100101
100101 100110 000011 100100 000100 000100 100011
100011 100100 000111 100000 100000 001000 011100
011100 011101 000001 011100 000100 000010 011010
011010 011011 000001 011010 000010 000010 011001
011001 011010 000011 011000 001000 000100 010110
010110 010111 000001 010110 000010 000010 010101
010101 010110 000011 010100 000100 000100 010011
010011 010100 000111 010000 010000 001000 001110


方法としては, yの右端が0の時, 最も右の`10'を'01'にする. yの右端が1の塊の時, その左の0を挟んだ左の1から右を01と1の塊にし, その右に0を詰める.
これをifで分岐せずに実行するのが味噌であるが, 説明は場合を分ける.

yの右端が0の時, y=α10b(b≥1), t=α10b-11, u=1, v=y, v and -v=10b, quotient (v and -v) (+ u 1)=10b-1; x=α010b-1.

yの右端が1の時, 1が全部右に寄ったら終りなので, 右の1の塊の左に0があり, その左に1があるとする. つまりy=α10a1b(a,b≥1)とする. t=α10a-110b, u=1b+1, v=α10a+b, v&-v=10a+b, u+1=10b+1 quotient (v&-v)(u+1)=10a-1, これをvから引くとα10a+b-10a-1=α01b+10a-1.

2010年2月11日木曜日

入れ子のかっこ

Lisp屋には違和感が全くない, 入れ子のかっこの話題だ. TAOCPにかっこの足跡(parenthesis trace)という話がある. (-sesと複数ではなく, -sisと単数なのはなぜか.)
例えば, 左かっこ4個, 右かっこ4個で, 正当なかっこの組み合せを作ると, ()()()(), ()()(()), ..., (((())))の14通りが出来る. n=4で14通りになるというのは, 2009年8月のブログ, 「投票数」に書いた通り.
8C4-8C3=70-56=14だ.

さて, かっこの足跡は左かっこを0, 右かっこを1で表した二進数である. 従って, ()()()()は01010101, (((())))は00001111となる. この方法で辞書式順で書くと

色の線で囲ったのは, 同じパターンが見えるところだ.

かっこ構造を保ったまま, 辞書式順で次の構造に移るにはどうするか. 二進法の表記では, 右から0と1を数えながら左へ`01'を探す. (0と1の数を#0, #1と書こう.) ただしこれまで通過した#0<#1でなければならない. そういう`01'を見つけたらそれを`10'と交換し, その右に#0だけ0を並べ, さらにその右に#1だけ1を並べる. これは辞書式順で最小の数を作るためである.

テストするには, 左から探したいので, 左右逆転のリストを使い,

(define x '(1 1 1 1 0 0 0 0))

(define (next x)
(let ((z 0) (o 0))
(define (dup n a) (if (= n 0) '()
(cons a (dup (- n 1) a))))
(define (nx x)
(if x (if (and (= (car x) 1) (= (cadr x) 0) (< z o))
(append (dup o 1) (dup z 0) '(0 1) (cddr x))
(begin (if (= (car x) 0)
(set! z (+ z 1)) (set! o (+ o 1)))
(nx (cdr x)))) 'ok))
(nx x)))

で(set! x (next x))を, x=okになるまで次々と実行する.

実はこれを一気に, 定数ステップで計算するアルゴリズムがあるらしい. これこそプログラムハックである.



μ0は...010101というパターンの二進数である.

その計算の進行の様子を下に示す.

上の段の左端は説明用の行数である. 左上のxで, 赤い`01'は交換する場所である. 次のozは, それより右の1と0の数. xのビット位置を右から0,1,2,...と数える. #0=#1となる可能性があるのは, 奇数番目の位置にある0なので, 3行目の右端の`01'の0, 8行目の`0101'の0が要注意だ. 奇数番目を取り出すべく, μ0を利用する.

tはxの右から見て最初の奇数桁目の1の右を0にする. uは反対に最初の奇数桁目の1の左を0に, 右を1にする. つまり右の1は要注意の0を隠す. この1の数は偶数だが, (#0+1)*2であることにも注意. vはuとxの∨でxの交換すべき`01'の右がすべて1になった. 交換すべき`01'の左は現状のまま. これに1を加えれば, `01'が`10'になり, 右は#0+#1個の0になる. 次のv∧¬wは右から#0+#1+1個の1なので, #0+1桁右シフトすれば, 右端に#1個の1が得られる. それが√(u+1)で割る理由である.

すばらしい!