[今日のPiet]GridPietGenerator入門編5(ユーザー入力1)

どもです。

GridPietGeneratorの 入門編、第5弾です。

今回のお題は時刻演算です。

なぜ入門編で時刻演算かって?

まだ紹介できていない命令を紹介するためです。

うまいこと盛り込もうとすると、時刻演算がぴったりな題材だったってだけです。

時間の足し算

突然ですが、「42分+37分は?」という問いに対して、

 A.1時間19分です。

 B.79分です。

 C.4740秒です。

という3通りの答え方ができます。

(Cの答え方は用法・用量を守らないと良からぬ結果を招きそうですが・・・。)

この計算をするプログラムを作ってみましょう。

ユーザーが2つの時間(分)を入力すると、

その和をAの方式で回答するプログラムを書いてみます。

方針

プログラムを作る前に方針を立てます。

  • はじめに、足し算する時間を、単位「分」で2つ入力します。
  • とりあえず足します。この結果をMとします。(この時点でB方式の回答が完成します。)
  • 出力する時間を「時間・分」形式で出力するため、つぎの計算をします。
    • Mを60で割ったhを求めます。これが「時間」の値です。
    • Mを60で割った余りmを求めます。これが「分」の値です。
  • 「hh:mm」の形式で出力します。

「入力」、「商」、「余り」、・・・。

筆者の腹の底が読みやすい方針ですね。

そこまでして新しい命令を詰め込みたいのかと。

プログラムと出力

まぁそれは置いておいて、この方針にしたがってプログラムを書くと、

つぎのようになります。

inn inn # minutes
add dup
push2 dup push3 push5 mul mul mul
dup
push3 push1 roll
mod
push3 push1 roll
div
outn
push1 push2 push3 dup mul mul add push3 mul push1 add
outc
outn
end

このプログラムをGridPietGenerator

Pietに変換するとつぎのようになります。

f:id:y-mos:20210823231735p:plain
時間の足し算プログラム

おや?いつになく横長ですね。

 

例によってnpietで実行してみます。 まず出力から。

./npiet addtime.ppm -tpic -tpf 16
? 42
? 37
1:19

このプログラムは、途中でユーザーに、足すべき時間(分)を入力するよう求めます。

npiet実行中に「?」が出てきて入力を促されるので、お好きな値を入力して

Returnキーなり、Enterキーなりを、押してください。

今回のケースでは2回入力を要求されます。

 

出力を見ると、「42分+37分=1時間19分」ですので、

答えはあっているようです。

 

f:id:y-mos:20210823231900p:plain
npietによるトレース結果

トレース画像を見てみると、今までとは少々違う結果が出ています。

なんと、Pietインタプリタが白色の領域をほとんど走っていません。

 

バグでしょうか。いいえ、ご安心下さい。お使いのGridPietGeneratorは正常です。

後ほどなぜこうなったかについても解説します。

解説

ではプログラムの解説です。

先ほどあげた方針に沿って解説します。

ユーザー入力

まず、冒頭にinn命令が2つ連続しています。

inn命令は、標準入力から数値を1つ読み取り、入力された値をスタックにプッシュします。

入力は数値として読み込める値でないといけません。

さもなくば、実行時エラーとなり、inn命令は無視されます。

 

今回のケースでは、ユーザーに2回数値の入力を求めており、

その値を順にスタックにプッシュしていることがわかります。

 

以下、ユーザーが初めに入力した値をX、次に入力した値をYとします。

2回のinn命令終了後のスタックは、つぎの状態になっています。

f:id:y-mos:20210823233549p:plain
 

とりあえず足す

とにもかくにもB方式の回答を作りましょう。

2行目のadd命令でXとYを足してMを作ります。

f:id:y-mos:20210823233845p:plain
 

時間・分への換算

今回の処理のメイン部分です。

先ほど作ったMに対して演算をして、時間や分を算出します。

まず、時間を計算するにせよ、分を計算するにせよ、

Mを使う必要があるので、Mをコピーして2つスタックに積んでおきます。

一方は「時間」の計算用、他方は「分」の計算用です。

f:id:y-mos:20210823234147p:plain
 

次に、割る数「60」を作ります。60=2\times{2}\times{3}\times{5}なので、

つぎの順で60を作ります。

f:id:y-mos:20210823234147p:plainf:id:y-mos:20210823234859p:plainf:id:y-mos:20210823234854p:plainf:id:y-mos:20210823234848p:plain
60を作る前の状態→push2→dup→push3

f:id:y-mos:20210823234843p:plainf:id:y-mos:20210823234832p:plainf:id:y-mos:20210823234826p:plainf:id:y-mos:20210823234819p:plain
push5→mul→mul→mul

(脱線)なぜ直接60をプッシュしないのか

さて、素朴な疑問として、なぜ60を直接プッシュしなかったのでしょうか?

3行目をまるっと置き換えて、つぎのようにしても同じ動きをするはずです。

inn inn # minutes
add dup
push60
dup
push3 push1 roll
mod
push3 push1 roll
div
outn
push1 push2 push3 dup mul mul add push3 mul push1 add
outc
outn
end

ご想像通り、このプログラムは機能的には正しく動きます。

一方で、生成されるPietソースコードはつぎのようになります。

f:id:y-mos:20210823235517p:plain
push60を含むPietソースコード

1か所、非常に大きなブロックが見えますね。

実は、まさにこの部分がpush60に対応する部分なのです。

 

実は、Pietのpush命令は、

「実行時に通過したカラーブロックの面積と同じ値をプッシュする」

という仕様になっています。

つまり、大きな値をプッシュしようとすると、

他の部分に比べて広い面積を持った同色領域を作る必要があるのです。

Pietの説明をろくにしてないのでわかりにくいかもしれませんが、

とにかく、大きな値をpushすると、大きなブロックができる

覚えておいてください。

 

GridPietGeneratorは、極力、

色ブロックの面積を小さくするよう設計されているため、

このような大きなブロックがあるとアンバランスに見えます。

そうならないように、大きな値を入力する時は、

なるべく小さな値の足し算・掛け算を駆使して、大きな値にしていくのが

常套手段となります。

(解説の続き)時間・分への変換

さて、本題に戻ります。

60ができたら、こちらも2回使うことになるので、

dup命令でコピーを作っておきます。

f:id:y-mos:20210823234819p:plainf:id:y-mos:20210824000341p:plain
コピー前→コピー後

また、この後の演算を実行するため、順番を入れ替えます。

順番の入れ替えにはroll命令を使うのでした。

roll命令は曲者なので、忘れてしまっていたら前回の解説をご参照ください。

ymos-hobby-programing.hatenablog.com

スタックの底からM、60、M、60の順に並べたいので、

つぎのように操作します。

f:id:y-mos:20210824000341p:plainf:id:y-mos:20210824000841p:plainf:id:y-mos:20210824204450p:plainf:id:y-mos:20210824000855p:plain
操作前→push3→push1→roll

深さnが3なので、スタック最上部から3つの値、

オレンジ色部分がroll操作の対象となります。

また、回数cが1なので、最上部の値を1回だけ、

オレンジ色部分の最下部に持っていけば良いことがわかります。

 

この操作により、最初に望んだ通り、

スタックの底からM、60、M、60の順に値を並べることができました。

 

今回はたまたま1回のroll操作で終了しましたが、

実際には入れ替え操作が1回では完了しないことも多く、

深さnや回数cを色々変えながら何度もroll操作を繰り返すこともよくあります。

分の計算

では「分」の値を計算しましょう。

これはMを60で割った余りから求まるのでした。

余りを求める命令は、mod命令です。

これは、スタック最上部の2つの値をポップして、最上部にあった値、その下にあった値で割って

その余りをスタックにプッシュします。

 

順番に気をつけてください。絵で説明すると、スタックが次の状態のとき:

f:id:y-mos:20210824001707p:plain
 

このときにmod命令を実行すると、計算結果はa\%bです。

a\%bは「aをbで割った余り」を表すとします。)

f:id:y-mos:20210824002018p:plain
 

 

mod命令の実行により、スタックはつぎの状態になります。

f:id:y-mos:20210824002218p:plainf:id:y-mos:20210824002528p:plain
mod実行前→mod実行後

これで「分」、つまり、mが計算できました。

この後、出力するまでmは使いませんし、「時間」も計算しなければいけません。

そこで、mをスタックの底に移動させて、

つぎの計算対象である、もう1組のMと60を、スタック最上部に持ってきます。

f:id:y-mos:20210824002949p:plainf:id:y-mos:20210824002847p:plainf:id:y-mos:20210824204755p:plainf:id:y-mos:20210824003031p:plain
m移動前→push3→push1→roll

深さn=3、回数c=1のroll命令を実行して、

スタックの最深部に「分」の値mを退避させます。

時間の計算

では「時間」の値を計算しましょう。

これはMを60で割った商(整数)から求まるのでした。

商(整数)を求める命令は、div命令です。

これは、スタック最上部の2つの値をポップして、最上部にあった値、その下にあった値で割って

その商(整数)をスタックにプッシュします。

 

この命令も順番に気をつけてください。絵で説明すると、スタックが次の状態のとき:

f:id:y-mos:20210824001707p:plain
 

このときにdiv命令を実行すると、計算結果はa/bです。

f:id:y-mos:20210824003536p:plain
 

割る順序は、mod命令と同じです。

 

div命令の実行により、スタックはつぎの状態になります。

f:id:y-mos:20210824003031p:plainf:id:y-mos:20210824003735p:plain
div実行前→div実行後

これで「時間」つまり、hが計算できました。

以上で計算は完了です。

出力する

あとは粛々と出力します。

まずはoutn命令で「時間」を出力します。

f:id:y-mos:20210824003908p:plainf:id:y-mos:20210824003904p:plain
outn実行前→outn実行後

次に区切りのコロン「:」を出力します。

コロンの文字コードは58ですが、先ほどのpush60の例を考えると、

こちらも分割した方が良さそうです。

筆者はつぎのように分解しました。分割の仕方はテキトーです。

58=(2\times{3}\times{3}+1)\times{3}+1

f:id:y-mos:20210824003904p:plainf:id:y-mos:20210824004537p:plainf:id:y-mos:20210824004548p:plainf:id:y-mos:20210824004555p:plain
「時間」出力直後→push1→push2→push3

f:id:y-mos:20210824004603p:plainf:id:y-mos:20210824004610p:plainf:id:y-mos:20210824004616p:plainf:id:y-mos:20210824004622p:plain
dup→mul→mul→add

f:id:y-mos:20210824004629p:plainf:id:y-mos:20210824004638p:plainf:id:y-mos:20210824004643p:plainf:id:y-mos:20210824004648p:plain
push3→mul→push1→add

あとは文字を出力するoutc命令で、コロン「:」を出力できます。

スタックは再びつぎの状態になります。

f:id:y-mos:20210824003904p:plain
 

最後にoutn命令で、残った時間「分」を出力すれば、完成です。

細長くなったPiet画像について

さて最後に、Piet画像が細長くなった理由について説明します。

そのために、まずGridPietGeneratorの 挙動について簡単に説明します。

 

GridPietGeneratorは、処理フローファイルが入力されると、

それを適当なところで分割してパーツに分けます。

それら各パーツをPiet画像化して、できたパーツを画像のフチに配置することで、

Pietソースコードに変換しています。

パーツからパーツへと移動するときに、

Pietインタプリタは画像中央の白色部分を縦横に走り回るのです。

 

そして、どこで分割するかは決まっていて、以下のどれかになります。

  1. ラベルの直前
  2. goto命令の直後
  3. if命令の直後
  4. end命令の直後

逆に、これら以外のところでは分割されません。

 

実は今回のプログラムですが、

ラベル、goto命令、if命令がありません。

end命令はありますが、その後ろに命令がありません。

したがって、このプログラムにはパーツが一つしかないのです。

 

パーツが一つしかないため、そのパーツを作ったら、

たった1つしかないパーツを配置して終了、

となってしまったのです。

 

このようにGridPietGeneratorは、ラベル、goto命令、if命令、end命令がないと

細長く一直線状の画像を出力してしまいます。

対策

実はこの現象には簡単な対策があります。

お気づきの方もおられるかもしれませんが、ないものは足せばいいのです。

つまり、ラベル、goto命令、if命令、end命令をわざと追加すれば、この「一直線現象」は回避できます。

しかし、むやみに命令を入れると、プログラムの構造を崩しかねません。

 

そこでおすすめなのが、ムダにラベルをつけることです。

ラベルはあくまでプログラムのジャンプ先を示すだけのもので、

それ以外にはプログラムになんの影響も与えません。

 

そこで、先ほどのプログラムに、わざとラベルを追加して、

つぎのようなプログラムに変えてみます。

inn inn # minutes
:add    #1
add dup
:make60 #2
push2 dup push3 push5 mul mul mul
dup
:min    #3
push3 push1 roll
mod
:hour   #4
push3 push1 roll
div
:outoutHour  #5
outn
:outputColon #6
push1 push2 push3 dup mul mul add push3 mul push1 add
outc
:outputMinutes  #7
outn
:end  #8
end

コメント「#1」〜「#8」の各行は、今回無駄に追加したラベルです。

それ以外は何も変えていません。

 

ラベルを追加した方のプログラムをGridPietGeneratorに入力すると、

つぎのような画像が出力されます。

f:id:y-mos:20210824010922p:plain
生成されたPiet画像(ラベル追加ver)

わお!見たことのある画像に変身しました。

 

この画像をnpietでトレースすると、

f:id:y-mos:20210824011105p:plain
npietによるトレース画像(ラベル追加ver)

今まで通り、画像中央を縦横に移動する様子が見られました。

当然ですが、処理内容・処理結果は冒頭のプログラムと全く同じです。

 

以上のように、ラベルを入れたことでパーツが増え、配置の仕方にバリエーションができ、

このようなGridPietGeneratorならではの画像を生成することができるのです。

このことから、ラベル、goto命令、if命令、end命令を積極的に使うと

出力結果はより望ましいものになることがわかります。

GridPietGeneratorを使うときは、これらをふんだんに使って、

複雑なアルゴリズムの設計に、ぜひチャレンジしてみてください。

最後に

本当は今回で命令を全て解説したかったけど無理でしたわ。

やっぱり時間と分量がかかりますね。

もう1回か2回くらいは時間かける必要があるかもです。はい。

 

じゃ、今回はこの辺で。

ではまた。