記号だけのPerlプログラミングの基本原理

2010-12-05

こんにちは。[/articles/advent-calendar/2010/casual/3:title=casual track 3日目]でも書かせていただきました、sugyanです。記号プログラミングはPerlくらいしかわからない素人ですが頑張って書いてみようと思います。よろしくおねがいします。

今日はPerlで記号プログラミングをするための基礎知識を説明します。Acme::EyeDropsでも使われているテクニックです。

■Step1. アルファベットの変換

記号だけでPerlプログラムを書きたい! というとき、最も邪魔なのがアルファベットですね。まずはこれらをどうにかして記号だけで表現しましょう。
Perlの文字列は2つの文字列の論理演算で表現することができます。例えば 'A' という文字はASCIIコード0x41ですね。これをASCIIコード0x60の '`' と0x21の '!' の排他的論理和で表現することができます。2進数で見ると分かりやすいですね。

   01100000  <- 0x60 '`'
^) 00100001  <- 0x21 '!'
-----------
   01000001  <- 0x41 'A'

したがって、'A'という文字列を、('`'^'!')と表現することが可能になります。ちなみに論理和の演算子が'|'、論理積の演算子が'&'、排他的論理和の演算子が'^'です。これは多くの言語で共通だと思います。
ためしにワンライナーで確かめてみましょう。

$ perl -le 'print "`"^"!"'
A

ちゃんと'A'が出力できますね。
この原理に従って、すべてのアルファベットは以下のように変換可能です。

A: ('`'^'!')
B: ('`'^'"')
C: ('`'^'#')
D: ('`'^'$')
E: ('`'^'%')
F: ('`'^'&')
G: ('`'^"'")
H: ('`'^'(')
I: ('`'^')')
J: ('`'^'*')
K: ('`'^'+')
L: ('`'^',')
M: ('`'^'-')
N: ('`'^'.')
O: ('`'^'/')
P: ('{'^'+')
Q: ('{'^'*')
R: ('{'^')')
S: ('{'^'(')
T: ('{'^'/')
U: ('{'^'.')
V: ('{'^'-')
W: ('{'^',')
X: ('{'^'#')
Y: ('{'^'"')
Z: ('{'^'!')
a: ('`'|'!')
b: ('`'|'"')
c: ('`'|'#')
d: ('`'|'$')
e: ('`'|'%')
f: ('`'|'&')
g: ('`'|"'")
h: ('`'|'(')
i: ('`'|')')
j: ('`'|'*')
k: ('`'|'+')
l: ('`'|',')
m: ('`'|'-')
n: ('`'|'.')
o: ('`'|'/')
p: ('['^'+')
q: ('['^'*')
r: ('['^')')
s: ('['^'(')
t: ('['^'/')
u: ('['^'.')
v: ('['^'-')
w: ('['^',')
x: ('['^'#')
y: ('['^'"')
z: ('['^'!')

これらを使って繋ぎ合わせることで、例えば'Hello world!'を出力するプログラムを

#!/usr/bin/perl
use strict;
use warnings;

print +('`'^'(').('`'|'%').('`'|',').('`'|',').('`'|'/').' '.('['^',').('`'|'/').('['^')').('`'|',').('`'|'$').'!';

と表現することができるようになります。
もちろん、上記の変換規則はあくまでも例で、組み合わせは1通りだけではありません。Perlのスローガン'TMTOWTDI'に則り自分なりの表現方法を考えてみても良いと思います:)

■Step2. evalで任意の文字列をコードとして実行

さて、文字列を記号だけで表現する方法は分かりましたが、任意のプログラムを実行するにはそれだけではどうしようもありません。
実際に記号だけでPerlプログラムとして実行可能なものを作るため、まず前段階としてevalを使用して実行したいコード全体を文字列として渡すようにします。上記のコードでいうとprint関数を含む行全体ということになりますね。
eval関数は引数として渡したブロックもしくは文字列をPerlプログラムとして解釈して実行してくれます。つまり、Step1で作成したHello worldのコードは

eval q{
use strict;
use warnings;

print +('`'^'(').('`'|'%').('`'|',').('`'|',').('`'|'/').' '.('['^',').('`'|'/').('['^')').('`'|',').('`'|'$').'!';
};

と表現することもできます(shebang行は省きました)。evalに文字列を渡しているだけのプログラムです。
引数は文字列なのでStep1で説明した通り、自由に変換することが可能です。まず

eval (('
use strict;
use warnings;

print').(' +"'.('`'^'(').('`'|'%').('`'|',').('`'|',').('`'|'/').' '.('['^',').('`'|'/').('['^')').('`'|',').('`'|'$').'!"'));

と既に記号化している部分を切り分け、まだアルファベットが残っている前半の部分をStep1の通りに変換してみましょう。

eval (
('['^'.').('['^'(').('`'|'%').' '.('['^'(').('['^'/').('['^')').('`'|')').('`'|'#').('['^'/').';'.
('['^'.').('['^'(').('`'|'%').' '.('['^',').('`'|'!').('['^')').('`'|'.').('`'|')').('`'|'.').('`'|"'").('['^'(').';'.
('['^'+').('['^')').('`'|')').('`'|'.').('['^'/').
(' +"'.('`'^'(').('`'|'%').('`'|',').('`'|',').('`'|'/').' '.('['^',').('`'|'/').('['^')').('`'|',').('`'|'$').'!"'));

2行目が'use strict;', 3行目が'use warnings;', 4行目が'print', 5行目で'"Hello world!"'を表しています。一文字ずつの対応を考えると何となく読めるような気もしてきますね!

■Step3. 拡張正規表現でevalを省く

Step2まででかなり記号化できましたが、evalが必要なのがどうにも困りものです。何とかしてevalを使わずに任意の文字列をコードとして実行するために、拡張正規表現の'(?{ code })'というパターンを使用します。
このパターンは'(?{ ... })'に書かれた任意のコードを実行することが出来るので、例えば

#!/usr/bin/perl
'hoge' =~ /(?{ print "fuga" })/;

というコードは'fuga'という文字列を出力してくれます。この場合マッチさせる対象である左辺'hoge'はぶっちゃけどうでもよくなりますね。
そして右辺の方も'//'で囲ってありますが、実はこのへんも文字列として表現しても問題ないようです。

#!/usr/bin/perl
'' =~ '(?{ print "fuga" })';

なんと、これでも'fuga'が出力されます。ということは、右辺文字列を分割して

#!/usr/bin/perl
'' =~ ('(?{'.'print "fuga"'.'})');

とすれば、'print "fuga"'の部分さえ記号にすれば、記号だけでプログラムを実行できるということになりますね。
Step2で作成したHello worldでevalに渡していた部分に置き換えてみましょう。

'' =~ ('(?{'.(
('['^'.').('['^'(').('`'|'%').' '.('['^'(').('['^'/').('['^')').('`'|')').('`'|'#').('['^'/').';'.
('['^'.').('['^'(').('`'|'%').' '.('['^',').('`'|'!').('['^')').('`'|'.').('`'|')').('`'|'.').('`'|"'").('['^'(').';'.
('['^'+').('['^')').('`'|')').('`'|'.').('['^'/').
(' +"'.('`'^'(').('`'|'%').('`'|',').('`'|',').('`'|'/').' '.('['^',').('`'|'/').('['^')').('`'|',').('`'|'$').'!"'))
.'})');

どうでしょう! これで記号だけで書かれたHello world!プログラムの出来あがりです。

■おまけ

上記プログラムがどのように解釈されて実行されているかを確かめることもできます。"-MO=Deparse"オプションをつけて先ほどのプログラムを実行してみましょう。

$ perl -MO=Deparse hello.pl
'' =~ /(?{use strict;use warnings;print +"Hello world!"})/;
hello.pl syntax OK

ちゃんと"Hello world!"をprintするようにコードが解釈されていることが確認できますね。

ところでこの任意のコードを実行する拡張正規表現、'perldoc perlre'を見ると

WARNING: This extended regular expression feature is
considered experimental, and may be changed without notice.
Code executed that has side effects may not perform
identically from version to version due to the effect of
future optimisations in the regex engine.

って書かれています。まぁマトモな使い道が思いつかないような謎の文法なんですが、これが使えなくなってしまったらどうやって記号プログラムを書けばいいんですかね…心配です。

■次回予告

まだ未定ですが

  • Acme::EyeDropsを使った簡単なPerl記号プログラムの作り方
  • より短いコードでPerl記号プログラムを作成するためのテクニック
  • より少い種類のPerl記号プログラムを作成するためのテクニック

あたりをネタとして用意しています。