[お知らせ]GridPietGeneratorアップデート
GridPietGeneratorを更新しました。
バグフィックス
- 一部処理系でビルド時にエラーとなるのを修正(返り値の指定忘れ)
引数の変更
- 入力ファイル名(処理手順を書いたテキストファイル)は、オプションなしで指定
- 出力ファイル名は下表のように指定
オプション | 説明 |
---|---|
-o , --output |
出力ppmファイル名を指定(Pietソースコード画像) |
-a , --ascii |
出力アスキーファイル名を指定(GridPietEmbedderの入力ファイル) |
-c , --csv |
デバッグ用csvファイルを指定 |
-l , --log |
ログファイル名を指定 |
例
- githubからGridPietGeneratorをダウンロードまたはクローン
- ビルド
- sampleフォルダに実行ファイルをコピー
- 使ってみる
終わりに
とりあえず報告だけアップします。 私の環境のVisual Studioではバグが発生しました。(普段Mac使いなもので気づいてませんでした。) ビルドに失敗していた方はもう一度やってみてください。
ではまた。
[Piet]10の階乗
今回のテーマは「10の階乗」です。 プログラミングでは定番の「再帰関数」をPietで実装します。
階乗
数学ででてくる「10!」というもので、 10から1までの整数全ての積のことです。 具体的に計算すると、
10!=10×9×8×...×2×1=3628800
となります。
再帰関数
プログラミングで使われる「再帰関数」とは、 内部で自分自身を呼び出すような関数です。
繰り返し処理の中には、「再帰関数」を利用すると、 プログラムの記述が非常に簡単になるものがあります。
先ほどの「階乗」は、
10!=10×9×8×...×2×1=10×(9×8×...×2×1)=10×9! 9!=9×8×7×...×2×1=9×(8×7×...×2×1)=9×8!
のように、
x!=x・(x-1)!
と書け、ある数の階乗計算には 元の数から1引いた数の階乗計算の結果を使えます。
こういう計算は再帰関数と相性がよいです。 例えばC++では次のように実装されます。
#include <iostream> int f(int x) { if(x==0){ return 1; } else { return x*f(x-1); } } int main() { std::cout << f(10) << std::endl; return 0; }
関数f
の中で、
- 引数
x
が0なら1を返す - 引数
x
が0でないならx*f(x-1)
を返す
というように、返り値にこの関数自身f
が含まれています。
ちなみに再帰関数を使わないと、for文で次のように書くことになります。
#include <iostream> int main() { int x=1; for(int i=10;i>0;i--) { x*=i; } std::cout << x << std::endl; return 0; }
まあ、このくらいなら正直どちらでも変わらない気もします。
しかし、処理内容によっては普通にループを書くより 再帰関数にした方が圧倒的に簡潔なので、 使いこなせることが好ましいテクニックです。
Pietで再帰関数
この程度なら難しくはありません。1
まずは方針を決めます。
- フラグ0と階乗を計算する値をプッシュしておく
- 〜ループ開始1〜
- 直前にプッシュした値から1を引いた値をプッシュする
- フラグ1をプッシュし、先ほどの値と順番を入れ替える
- 先ほどプッシュした値が0でないなら「〜ループ開始1〜」に戻る
- 最後にプッシュしたフラグと値を削除
- 〜ループ開始2〜
- フラグをチェックして0ならループをぬけ「〜終了処理〜」へ
- スタックの最上部にある2つの数値を掛け算する
- 「〜ループ開始2〜」へ
- 〜終了処理〜
- 計算結果を表示し、終了
Pietソースコード
では実際に書いてみます。 まずは処理順から。 今回は短いのでそのまま載せます。
命令の詳細はこちら。
ymos-hobby-programing.hatenablog.com
ymos-hobby-programing.hatenablog.com
push1 dup sub push2 push5 mul goto:fact :factR outn pop end :fact dup push1 sub push1 push2 push1 roll dup dup dup sub gt if:factN:factMulP :factN goto:fact :factMulP pop pop :factMul push2 push1 roll if:factMulN:factR :factMulN mul goto:factMul
これをGridPietGeneratorで処理した結果がこちら。
GridPietGeneratorについてはこちらの記事を参照。
ymos-hobby-programing.hatenablog.com
ymos-hobby-programing.hatenablog.com
コーデル数は51x53と小さめです。
ちゃんとnpietでも動きます。
./npiet -cs 10 factorialx10.png 3628800
処理解説
では処理の解説です。例によって処理順はコロン「:」で始まる行で区切られており、 それぞれが処理のひとかたまりになっています。
ちなみに生成されたソースコードに対応させるとこんな感じです。 このように、GridPietGeneratorで生成されたコードは、処理ブロックのカタマリが 画像のフチに配置されます。
これらの処理順を図示すると次のようになります。極めてシンプルです。
処理順を解説していきます。
スタックの初期化
まずは「:1」ブロックでスタックを初期化します。
初期化時のスタックの変化は下図の通りです。
- 灰色部分がスタック(=データを入れる容器)
- 文字のついた白い長方形がデータ
- スタックのデータ出入り口は上がわ
と考えてください。
最終的にスタックには0と10が積まれます。
- 下の0はフラグです(後で説明します)
- 上の10は階乗計算の対象となる数値10です
10, 9, 8, ... と数値をスタックに積んでいく処理
次に:fact
と:factN
の部分です。
この部分では数値1ずつ減らしながらスタックにプッシュしていきます。
具体的なスタックの変化は次のようになります。
スタックには既にフラグfと数値xの組(f、x)がいくつか入っていて、そのうちの最上部の1組だけを表示したのが上の図だと考えてください。
まずは今スタックに入っている値x
をコピーして、x-1
をスタックにプッシュします。
さらに新たにフラグ1
をプッシュして順番を入れ替え、初期化の時と同じ順番、つまり、
- 下がフラグ
- 上が値
となるようにします。
続いてこの部分で、現在の値x-1
が0より大きいか判定します。
比較に使う0を作るために、x-1
をdup
命令で3回も複製し、
うち2つを引き算して0をつくっています。
最後の部分は次の判定をif命令で実行しています。
x-1
が0より大 →:fact
に戻って繰り返す 2x-1
が0 → 次の処理である:factMulP
に進む
この部分を繰り返すことで、スタックには、下から、
フラグf(=0)、10、フラグf(=1)、9、...、フラグf(=1)、1、フラグf(=1)、0
と値がプッシュされていきます。 フラグを挟んで階乗計算に必要な数列{10, 9, ..., 1}が用意できました。
階乗計算
いよいよ階乗計算です。
まず準備として、:factMulP
ブロックでは、スタックの最上部の2つの値をポップします。
この2つの値は使わないからです。
続いて終了判定と掛け算を繰り返す部分です。:factMul
ブロックと:factMulN
ブロックで
その処理をしています。
:factMul
ブロックでは、終了判定を行なっています。
まず上2つだけに注目して、roll
命令で順番を入れ替えます。
フラグがスタックの最上部に来たところで、if命令を使い、
- フラグ
f
が1なら継続 →:factMulN
ブロックに進む - フラグ
f
が0なら終了 →:factR
ブロックに進む (=ループ脱出)
とします。
初期化時や:fact
ブロックでプッシュしていたフラグは、
ここで処理順を決めるために使われます。
継続の場合は、:factMulN
ブロックで、スタック最上部の2数を掛け算します(mul
命令)。
上図のスタック変化には、フラグf'
のさらに下にあった、
フラグf''
と数値データx'
も表示しています。
掛け算の結果、スタック最上部は、
...、フラグf''、数値x'、フラグf'、数値x X
となり、フラグと数値が交互に現れます。
このまま:factMul
に戻れば、スタックにプッシュされている数値が順々に
掛け算されていくという算段です。
ループ終了
掛け算のループは初期化時にプッシュしたフラグ0 に到達すると終了します。
このときの:factMul
ブロックでの処理を見てみましょう。
このときはif命令で、初期化時にプッシュしたフラグfが読み込まれますが、
このfの値は0なので:factR
ブロックに進みます。
ここまででスタックには、計算結果、つまり、10、9、...、1までの
全ての数値の積X
のみが格納されています。
その結果を:factR
ブロック内のoutn
命令で出力して終わり、となります。3
以上が10の階乗プログラムの説明です。
最後に
「再帰関数」と銘打ちながら、あまり再帰関数らしさが感じられませんでした。 正直、階乗の計算ならループでいけますし。。。
さて、前回お話ししていたように、今回はスタックの変化がわかるように図にしてみました。 正確にいうと、この図を半自動で作るツールを作り、今回初めて使ってみました。 説明の手間も省けますし、見るほうも図があると多少わかりやすいと思うので、 自分としてはよかったと思っています。
次回もまた適当にネタを探してPietで実装したいと思います。
ではまた。
[Piet]ASCIIコード表
Pietでプログラミングしていると文字出力のためにASCIIコード表が欲しくなります。表を作ること自体は難しくありません。でもめんどくさい。
そういう面倒なことはプログラムにお任せするのが良いと思います。というわけでPietで書いてみました(なぜ)
概要
調べてみるとASCIIコードの印字可能文字は、16進数で 0x20-0x7Fの範囲にあるそうで、とりあえずこの 範囲の表ができれば問題ありません。
出力されるASCIIコード表は次のようなものにします。
|' ' (32)|'!' (33)|'"' (34)|'#' (35)|'$' (36)|'%' (37)|'&' (38)|''' (39)|'(' (40)|')' (41)|'*' (42)|'+' (43)|',' (44)|'-' (45)|'.' (46)|'/' (47)| |'0' (48)|'1' (49)|'2' (50)|'3' (51)|'4' (52)|'5' (53)|'6' (54)|'7' (55)|'8' (56)|'9' (57)|':' (58)|';' (59)|'<' (60)|'=' (61)|'>' (62)|'?' (63)| |'@' (64)|'A' (65)|'B' (66)|'C' (67)|'D' (68)|'E' (69)|'F' (70)|'G' (71)|'H' (72)|'I' (73)|'J' (74)|'K' (75)|'L' (76)|'M' (77)|'N' (78)|'O' (79)| |'P' (80)|'Q' (81)|'R' (82)|'S' (83)|'T' (84)|'U' (85)|'V' (86)|'W' (87)|'X' (88)|'Y' (89)|'Z' (90)|'[' (91)|'\' (92)|']' (93)|'^' (94)|'_' (95)| |'`' (96)|'a' (97)|'b' (98)|'c' (99)|'d'(100)|'e'(101)|'f'(102)|'g'(103)|'h'(104)|'i'(105)|'j'(106)|'k'(107)|'l'(108)|'m'(109)|'n'(110)|'o'(111)| |'p'(112)|'q'(113)|'r'(114)|'s'(115)|'t'(116)|'u'(117)|'v'(118)|'w'(119)|'x'(120)|'y'(121)|'z'(122)|'{'(123)|'|'(124)|'}'(125)|'~'(126)|''(127)|
文字をシングルクォーテーション'
でくくり、カッコでASCIIコードの数値を10進数で示します。
表全体は垂直バー|
で区切って表現します。Pietでシンプルに表を出力するならこんなもんでしょう。
1行は16文字とします。 1行めが文字コード0x20-0x2Fに対応、2行めが文字コード0x30-0x3Fに対応、・・・とします。
プログラムのフロー
- 表の縦方向のループカウンタ
y
を用意する - 縦方向ループ開始
- 表の横方向のループカウンタ
x
を用意する - 横方向ループ開始
- 表示する文字コードを計算(
x+16*y
) - 垂直バーを表示(
|
) - 文字を表示(
'文字'
) - 文字コードを表示(
(文字コード)
) x
を更新(x<-x+1
)x
<16なら「横方向ループ開始」に戻る- 垂直バーと改行文字を出力する(次の行へ)
y
を更新(y<-y+1
)y
<8なら「縦方向ループ開始」に戻る- 終了
プログラム
まずは作ったプログラムと自動生成したPietソースコードを示します。プログラムは長いので、見たい方は下の文字をクリックして開いてください。
クリックしてプログラムをみる
push2 :loopY push1 dup sub :loopX dup push3 push2 roll dup push4 push1 roll push2 dup mul dup mul mul add push1 goto:out_bar :out_bar_return1 pop goto:out_char :out_char_return goto:out_num :out_num_return pop push1 add dup push3 push5 mul gt if::loopX push2 goto:out_bar :out_bar_return2 pop goto:out_NL :out_NL_return push2 push1 roll push1 add dup push3 push2 dup add add gt if:end: push2 push1 roll pop goto:loopY :out_char dup push2 dup dup dup dup push1 add add mul mul mul push1 sub dup push3 push1 roll outc outc outc goto:out_char_return :out_bar push2 dup dup dup mul mul dup mul mul push2 dup mul sub outc :out_bar_return dup push1 sub if::out_bar_return1 dup push2 sub if::out_bar_return2 end :out_num dup push2 dup dup push1 add add mul dup mul push1 sub gt if:out_num_out: push1 goto:out_space :out_space_return1 pop :out_num_out dup push2 dup dup mul mul push2 push3 add mul dup push1 add push3 push1 roll outc outn outc goto:out_num_return :out_space push2 dup dup mul dup mul mul outc :out_space_return dup push1 sub if::out_space_return1 end :out_NL push2 push5 mul outc goto:out_NL_return :end end
これをnpietで実行すると次のようになります
> ./npiet -cs 6 asciix6.png |' ' (32)|'!' (33)|'"' (34)|'#' (35)|'$' (36)|'%' (37)|'&' (38)|''' (39)|'(' (40)|')' (41)|'*' (42)|'+' (43)|',' (44)|'-' (45)|'.' (46)|'/' (47)| |'0' (48)|'1' (49)|'2' (50)|'3' (51)|'4' (52)|'5' (53)|'6' (54)|'7' (55)|'8' (56)|'9' (57)|':' (58)|';' (59)|'<' (60)|'=' (61)|'>' (62)|'?' (63)| |'@' (64)|'A' (65)|'B' (66)|'C' (67)|'D' (68)|'E' (69)|'F' (70)|'G' (71)|'H' (72)|'I' (73)|'J' (74)|'K' (75)|'L' (76)|'M' (77)|'N' (78)|'O' (79)| |'P' (80)|'Q' (81)|'R' (82)|'S' (83)|'T' (84)|'U' (85)|'V' (86)|'W' (87)|'X' (88)|'Y' (89)|'Z' (90)|'[' (91)|'\' (92)|']' (93)|'^' (94)|'_' (95)| |'`' (96)|'a' (97)|'b' (98)|'c' (99)|'d'(100)|'e'(101)|'f'(102)|'g'(103)|'h'(104)|'i'(105)|'j'(106)|'k'(107)|'l'(108)|'m'(109)|'n'(110)|'o'(111)| |'p'(112)|'q'(113)|'r'(114)|'s'(115)|'t'(116)|'u'(117)|'v'(118)|'w'(119)|'x'(120)|'y'(121)|'z'(122)|'{'(123)|'|'(124)|'}'(125)|'~'(126)|''(127)|
最後のASCIIコード127は実は印字できない文字なので、
ここだけ垂直バー|
がずれているのはご愛嬌です。
プログラムの構造解説
さて、プログラムにはコロン「:」で始まる行がありますが、 それが概ねプログラムの処理のひとかたまりを表します。
それらがどのような順で実行されるかを示したものが下図になります。
詳しく解説します。
プログラム開始地点
まず、プログラムの開始地点は「:1」というブロックです。 そこから表の縦方向(行)のループの開始地点「:loopY」と、横方向(列)のループの開始地点「:loopX」を通ります。
この間、スタックにはループカウンタの役割を果たす値がプッシュされます。
文字コード計算と出力
次に、「表示する文字コードを計算」から「文字コードを表示」までの処理です。次の部分にあたります。
例えば、文字コードが100(英小文字d
)のときは、この処理で次の文字列が出力されます。
|'d'(100)
各ブロックの処理は次のとおりです。
ブロック名 | 処理説明 |
---|---|
:loopX | 文字コードの計算(X+16*Y ) |
:out_bar | 垂直バー| の出力 |
:out_bar_return | 次の処理の決定 |
:out_bar_return1 | (次の処理) |
:out_char | 文字出力(例:'d' ) |
:out_char_return | (次の処理) |
:out_num, :out_num_out | 文字コードの出力(例:(100) ) |
改行判定〜改行しない場合
次に、「:out_num_return」というブロックで改行判定をします。
まだ1行ぶんの出力が終わっていないなら改行不要と判定されます。この場合は、横方向のループカウンタx
に1を足して、「:loopX」に戻り、
次の文字の出力を続けます。
このループで表の1行が出来上がっていきます。 ただし、右端に垂直バーが出ないことに注意です。
|'`' (96) |'`' (96)|'a' (97) |'`' (96)|'a' (97)|'b' (98) |'`' (96)|'a' (97)|'b' (98)|'c' (99) |'`' (96)|'a' (97)|'b' (98)|'c' (99)|'d'(100) |'`' (96)|'a' (97)|'b' (98)|'c' (99)|'d'(100)|'e'(101) ...
改行判定〜改行する場合
1行ぶんの表示が終わった場合は、改行します。
ただし、改行の前に表の右端を閉じるため垂直バー|
を出力します。
垂直バーを出力する処理は「:out_bar」ブロックで一度書いたので、使い回すことにします。
その後、ループカウンタを更新(y
に1を足し、x
を0にする)して、横方向のループを再開します。
改行処理は次の部分で行なっています。
ブロック名 | 処理説明 |
---|---|
:out_num_return | 文字コード出力後の処理先兼、改行判定 |
:out_bar | 垂直バー| の出力 |
:out_bar_return | 次の処理の決定 |
:out_bar_return2 | (次の処理) |
:out_NL | 改行文字出力 |
:out_NL_return | (次の処理) |
改行する前に、「:out_num_return」ブロックから「:out_bar」に入り、垂直バー|
を出力します。
同じ「:out_bar」の処理でも、「:loopX」から入った時と、「:out_num_return」から入った時とで、処理ルートを変える必要があります。そのために「:out_bar」に入る直前にスタックにフラグをプッシュしておき、その値に応じて行き先を変えます。
プログラムをみると、「:loopX」と「:out_num_return」の最後はいずれもgoto:out_bar
です。
その直前で、「:loopX」ではpush1
を、「:out_num_return」ではpush2
を実行して、フラグをプッシュしています。
「:out_bar_return」では、フラグが1なら「:out_bar_return1」に進み、フラグが2なら「:out_bar_return2」に進むように設計されているため、処理ルートを変えることができます。
Pietはスタックが唯一のメモリですので、同じ処理を使い回す場合は、フラグを使って処理順序を変えます。
終了判定
「:out_NL_return」ブロックは、プログラムの終了判定もしています。ここでは、縦方向ループカウンタy
の値をチェックし、7を超えたら終了、「:end」に進みます。
おまけ〜表の見栄えを良くする
ここまでの説明に出てこなかった下図の部分の補足です。
この部分は、表の1マスのサイズを揃えるために、 半角スペースを出力する処理をしています。
この処理が抜けると、下に示すように、 文字コードの数値が2桁か3桁かによって表のマスの長さが変わり、垂直バーの位置がずれてしまいます。 (右端までスクロールするとわかりやすいです。申し訳ないですが、スマホだとわからないかもしれません・・・。)
|' '(32)|'!'(33)|'"'(34)|'#'(35)|'$'(36)|'%'(37)|'&'(38)|'''(39)|'('(40)|')'(41)|'*'(42)|'+'(43)|','(44)|'-'(45)|'.'(46)|'/'(47)| |'0'(48)|'1'(49)|'2'(50)|'3'(51)|'4'(52)|'5'(53)|'6'(54)|'7'(55)|'8'(56)|'9'(57)|':'(58)|';'(59)|'<'(60)|'='(61)|'>'(62)|'?'(63)| |'@'(64)|'A'(65)|'B'(66)|'C'(67)|'D'(68)|'E'(69)|'F'(70)|'G'(71)|'H'(72)|'I'(73)|'J'(74)|'K'(75)|'L'(76)|'M'(77)|'N'(78)|'O'(79)| |'P'(80)|'Q'(81)|'R'(82)|'S'(83)|'T'(84)|'U'(85)|'V'(86)|'W'(87)|'X'(88)|'Y'(89)|'Z'(90)|'['(91)|'\'(92)|']'(93)|'^'(94)|'_'(95)| |'`'(96)|'a'(97)|'b'(98)|'c'(99)|'d'(100)|'e'(101)|'f'(102)|'g'(103)|'h'(104)|'i'(105)|'j'(106)|'k'(107)|'l'(108)|'m'(109)|'n'(110)|'o'(111)| |'p'(112)|'q'(113)|'r'(114)|'s'(115)|'t'(116)|'u'(117)|'v'(118)|'w'(119)|'x'(120)|'y'(121)|'z'(122)|'{'(123)|'|'(124)|'}'(125)|'~'(126)|''(127)|
これを防ぐため、文字コードが2桁の場合は、文字出力('c'
など)の後ろに半角スペース' '
を1文字出力してから、文字コードの出力((99)
など)をしています。これで垂直バーの位置ずれを防ぐことができます。
おわりに
今回は、プログラムの流れを図示しながら解説してみました。 ブログの更新を1週間サボってこれらの図を半自動で作成できるツールを作っていました。 おかげで説明する側としても労力がだいぶ減りましたし、 多少なりともわかりやすくなっていれば良いと思います。
今回はスタックの細かい変化は省きましたが、 Pietでプログラミングするときにはスタックの変化も追っていく必要があります。 今後はスタックの変化も図にできたらいいなあと思いつつ、 図示用ツールを作成中です。
次回は何を題材にしようかちょっと迷ってますが、 算数・数学チックな話になるのではないかと思います。 (素数判定とか、分数計算とか)
ではまた。