2017/08/09 00:04

この記事は、medium.com からの転載です。

何回か、計算という概念にかかわる話題を書こうかと思います。

sedで計算するというと、どういうことかとも思うかもしれません。sedの処理自体が計算なのですから。

そこで、足し算を例に挙げて見ます。きっちり計算することもできますが、ここではわかりやすさのために超手抜き版として、1~9の任意の1桁の数字の足し算とします。

では、1つめです。コードのファイル名は “plus1.sed” とします:
s/1/I/g
s/2/II/g
s/3/III/g
s/4/IIII/g
s/5/IIIII/g
s/6/IIIIII/g
s/7/IIIIIII/g
s/8/IIIIIIII/g
s/9/IIIIIIIII/g
s/ *\+ *//g

これに、このようなデータを与えます。ファイル名は “plus.dat” とします:
1 + 4
3 + 3
9 + 9
1 + 2 + 3
7 + 8 + 9

では、実行してみましょう:
$ sed -r -f plus1.sed plus.dat
IIIII
IIIIII
IIIIIIIIIIIIIIIIII
IIIIII
IIIIIIIIIIIIIIIIIIIIIIII

1〜9を対応する数の “I” に置き換えて、 “+” あたりを消しているだけです。それでも、 “1 + 4” の結果が “5” に、 “3 + 3” の結果が “6” に、 “9 + 9” の結果が “18” に、 “1 + 2 + 3” の結果が “6” に、 “7 + 8 + 9” の結果が “24” になっています。

これではわかりにくい? では “plus2.sed” のコードを使ってみましょう:
s/1/I/g
s/2/II/g
s/3/III/g
s/4/IIII/g
s/5/IIIII/g
s/6/IIIIII/g
s/7/IIIIIII/g
s/8/IIIIIIII/g
s/9/IIIIIIIII/g
s/ *\+ *//g
s/IIIIIIIIII/X/g
s/IIIII/V/g

これを実行するとこうなります:
$ sed -r -f plus2.sed plus.dat
V
VI
XVIII
VI
XXIIII

まじめに計算するためには、 “a + b” とあった場合、 “a + 1” の計算と、 “b — 1” の計算をし、bの方が0になったら計算を終えるようにする必要があります。それだと、たとえば “3 + 4” の場合、“3 + 1” を行ない、 “4–1” の計算もしてやり、ループすることで結果を得られます。また、その方法を取った場合、1~9の数字のままで処理をしたり、2桁以上の数同士の足し算もそのままできるなどの利点もあります。 ですが、そのままコードも面倒になります。ですので、ここではこの例に留めます。

こっちの方が簡単であるということとともに、この例の場合で都合がいいのが、「こんなものが計算と呼べるのか」という疑問を喚起するかもしれないという点です。ですが、 “plus1.sed” の場合でも、きちんと結果は出ています。それでもこういう声も聞こえてきそうです:
「結果を人間が数えなきゃならないじゃないか」

そういう文句はアラン・チューリングに言ってください。チューリング・マシンの結果というのも、こんな感じだったりします。

ですが、そういう疑問も持ってもらうのはいいことかなと思います。小中高でプログラミング教育に関わっている方にとっては特に。もしかしたらこれを計算と認めるのには抵抗があるかもしれません。ですが、きちんと計算をしています。計算というのは、おそらく普通に思われているだろうものよりも、範囲が広いものです。

なお、sedでの高機能なスクリプトはあちこちで公開されていますので、興味があれば検索して、確認してみてください。

まぁ、上の例ではあんまりだということであれば、こういうのはどうでしょうか。

1桁の数の足し算に限定するという条件は変わりませんが、 “a + b” に対して “a + 1” と “b - 1” をするという方向を見てみましょう。その変化が見えるようにしているコードの例を “plus3.sed” とします。それがこちら:
p
s/1/I/g
s/2/II/g
s/3/III/g
s/4/IIII/g
s/5/IIIII/g
s/6/IIIIII/g
s/7/IIIIIII/g
s/8/IIIIIIII/g
s/9/IIIIIIIII/g
s/\+/ /g
p
:loop
/^I+ +I+/ {
s/I +I/II /
p
bloop
}
s/^(.)/=\1/

これを “plus.dat” にあてると、こうなります:
$ sed -r -f plus3.sed plus.dat
1 + 4
I IIII
II III
III II
IIII I
IIIII
=IIIII
3 + 3
III III
IIII II
IIIII I
IIIIII
=IIIIII
9 + 9
IIIIIIIII IIIIIIIII
IIIIIIIIII IIIIIIII
IIIIIIIIIII IIIIIII
IIIIIIIIIIII IIIIII
IIIIIIIIIIIII IIIII
IIIIIIIIIIIIII IIII
IIIIIIIIIIIIIII III
IIIIIIIIIIIIIIII II
IIIIIIIIIIIIIIIII I
IIIIIIIIIIIIIIIIII
=IIIIIIIIIIIIIIIIII
1 + 2 + 3
I II III
II I III
III III
IIII II
IIIII I
IIIIII
=IIIIII
7 + 8 + 9
IIIIIII IIIIIIII IIIIIIIII
IIIIIIII IIIIIII IIIIIIIII
IIIIIIIII IIIIII IIIIIIIII
IIIIIIIIII IIIII IIIIIIIII
IIIIIIIIIII IIII IIIIIIIII
IIIIIIIIIIII III IIIIIIIII
IIIIIIIIIIIII II IIIIIIIII
IIIIIIIIIIIIII I IIIIIIIII
IIIIIIIIIIIIIII IIIIIIIII
IIIIIIIIIIIIIIII IIIIIIII
IIIIIIIIIIIIIIIII IIIIIII
IIIIIIIIIIIIIIIIII IIIIII
IIIIIIIIIIIIIIIIIII IIIII
IIIIIIIIIIIIIIIIIIII IIII
IIIIIIIIIIIIIIIIIIIII III
IIIIIIIIIIIIIIIIIIIIII II
IIIIIIIIIIIIIIIIIIIIIII I
IIIIIIIIIIIIIIIIIIIIIIII
=IIIIIIIIIIIIIIIIIIIIIIII

“plus.dat” の各行を表示し、また結果には “=” をつけて表示しています。 “1 + 4” を例に見てみましょう。最初は:
I IIII

こうなっています。それが出力の次の行ではこうなっています:
II III

“I” が “II” になり、 “IIII” が “III” になっています。以下、これのループで、 “a + b” の “b” の位置に “I” が一つもなくなると:
/^I+ +I+/

この、スペースの左側に “I” が1個以上ならんでおり、かつスペースの右にも “I” が1個以上並んでいるという条件が成立しなくなり、一行の処理が終わります。
肝と言えるのは、この部分です:
s/I +I/II /

スペースの左側の “I” を “II” に書き換え、またスペースの右側の “I” を一個消しています。ここで注目しているのはスペースのすぐ左側の “I” 1個だけであり、またスペースのすぐ右側の “I” 1個だけです。ですが両方とも1個以上並んでいる条件ですから、その条件のもと、スペースの右側の “I” 1個を、スペースの左側の “I” の並びに移動したと読めます。あるいは読みます。

読みやすさのために、 “plus4.sed” も挙げておきましょう:
p
s/1/I/g
s/2/II/g
s/3/III/g
s/4/IIII/g
s/5/IIIII/g
s/6/IIIIII/g
s/7/IIIIIII/g
s/8/IIIIIIII/g
s/9/IIIIIIIII/g
s/\+/ /g
p
:loop
/^I+ +I+/ {
s/I +I/II /
p
bloop
}
s/IIIIIIIIII/X/g
s/IIIII/V/g
s/^(.)/=\1/

これを “plus.dat” にあてると、こうなります:
$ sed -r -f plus4.sed plus.dat
1 + 4
I IIII
II III
III II
IIII I
IIIII
=V
3 + 3
III III
IIII II
IIIII I
IIIIII
=VI
9 + 9
IIIIIIIII IIIIIIIII
IIIIIIIIII IIIIIIII
IIIIIIIIIII IIIIIII
IIIIIIIIIIII IIIIII
IIIIIIIIIIIII IIIII
IIIIIIIIIIIIII IIII
IIIIIIIIIIIIIII III
IIIIIIIIIIIIIIII II
IIIIIIIIIIIIIIIII I
IIIIIIIIIIIIIIIIII
=XVIII
1 + 2 + 3
I II III
II I III
III III
IIII II
IIIII I
IIIIII
=VI
7 + 8 + 9
IIIIIII IIIIIIII IIIIIIIII
IIIIIIII IIIIIII IIIIIIIII
IIIIIIIII IIIIII IIIIIIIII
IIIIIIIIII IIIII IIIIIIIII
IIIIIIIIIII IIII IIIIIIIII
IIIIIIIIIIII III IIIIIIIII
IIIIIIIIIIIII II IIIIIIIII
IIIIIIIIIIIIII I IIIIIIIII
IIIIIIIIIIIIIII IIIIIIIII
IIIIIIIIIIIIIIII IIIIIIII
IIIIIIIIIIIIIIIII IIIIIII
IIIIIIIIIIIIIIIIII IIIIII
IIIIIIIIIIIIIIIIIII IIIII
IIIIIIIIIIIIIIIIIIII IIII
IIIIIIIIIIIIIIIIIIIII III
IIIIIIIIIIIIIIIIIIIIII II
IIIIIIIIIIIIIIIIIIIIIII I
IIIIIIIIIIIIIIIIIIIIIIII
=XXIIII

結果は同じですし、処理の回数は無駄と言えば無駄に増えていますが、こっちの方が計算っぽいと思えるかもしれません。

なお、数学的には、あるいは数学的な方法の一つとしては、 “a + b” というのは、このように書けます:
while (b > 0) {
a = succ(a)
b = b - 1
}

ここの “succ” というのは、その引数である “a” の、整数における次の値を返す関数です。ですので、 “succ(a)” というのは “a + 1” という意味です。実際には数学だと再帰の書きかたのほうが好まれます。その場合、 “a” や “b” でもかまわないのですが、とくに “b” については具体的な数字のほうが分かりやすかったりするかと思うので、ここでは “2 + 3” ということで考えてみます:
succ(succ(succ(2)))

これはつまり、こういう意味です:
2 + 1 + 1 + 1
= 2 + 3

“succ” の回数分、 “1” を足しているので、こういう形になります。

2つめというか後半で示したスクリプトは、この形式に沿っているとも言えることがわかるかと思います。

なお、 “succ()” という関数は非常に重要で、 “1 + 1 = 2” の証明も、この関数をまず定義しています。というか、「次の値とはなにか」も含めて、 “succ()” の定義が証明のほとんどを占めています。

この “succ()” という関数は、lispなどの元にある計算モデルであるλ計算においても重要な関数です。λ計算が昔に構築されていたら、 “1 + 1 = 2” の証明も楽勝だったのにと言われたりします。