2012年2月27日月曜日

[Glaeja] エスケープ・フロム・Glaeja

というわけで、『Glaeja』においてエスケープキャラクタを含む文字列を解析・置換する「文字列解析エンジン」の動作仕様と、「文字列解析制御エスケープキャラクタ」の解説のためのエントリです。

できるだけわかりやすく書くよう心がけますが、このエントリを読んで理解できなかった方は、「文字列解析制御エスケープキャラクタ」の使用を諦めてください。使い方間違えるとマジで簡単に無限ループに入っちゃってバンバン強制終了しますから…

そして、仮に理解できなかったとしても、決してそれを恥じないでください。こんなもん理解できて使える方がオカシイんです!




1.『Glaeja』における「文字列解析エンジン」

『Glaeja』における「文字列解析エンジン」は、「テキスト」レイヤーの[表示文字列]や、[バー]レイヤーの[現在値となる項目]、[イメージ]レイヤーの[パターン文字列]等の文字列を入力できる項目において、入力された文字列(元文字列)を解析し、含まれるエスケープキャラクタを展開・置換し、ウィジェット描画に用いられる結果(結果文字列)を出力するものです。

…毎度のことですが、何を言ってるか意味がわかりませんねw 文字列解析エンジンの動作を理解するために、頭の中でどんなイメージを描けばいいか、ちょっとイラストを書いてみましょう。


…こんな感じですかねw
イラストの上にならんでる文字の書かれた紙テープが「元文字列」、下の紙テープが「結果文字列」です。真ん中にある灰色のボックスが文字列解析エンジンで、その右がエンジンを操作する棒人間ですw エンジンの上部には窓があり、窓の位置(カーソル位置)に来た元文字列の文字を読み、その文字に応じた文字が末尾に追記された紙テープが出力される、みたいなイメージですね。

大事なのは、「元文字列と結果文字列という2つ文字列がある」「元文字列は(基本的には)読まれるだけ」ということです。

2.「文字列解析エンジン」の動作手順

では、この元文字列「Today=$MMM.dd$@0/2/\o@」について、文字列解析エンジンの動作を一から追ってみましょう。

0.最初にエンジンを元文字列の1文字目にセットします。

1.窓(カーソル位置)の文字がエスケープキャラクタ開始文字($, #, %, ...)でなければ、その文字を結果文字列の末尾に出力します

2.エンジンを次の文字へ移動し、1.をエスケープキャラクタ開始文字になるまで繰り返します。

3.カーソル位置に(@|以外の)エスケープキャラクタ開始文字がきたら、棒人間は次の同じエスケープキャラクタ終了文字までに囲まれた部分を読んで、それをエスケープキャラクタに応じた文字列に展開し、結果文字列に追記します。

4.エンジンをエスケープキャラクタ終了文字の次の文字へ移動し、1.(もしくは3.)へ戻ります。

5.カーソル位置のエスケープキャラクタ開始文字が「@」だった場合、棒人間は次の同じエスケープキャラクタ終了文字(「@」)までに囲まれた部分を読んで、その指示内容に従って結果文字列を置換します。

6.エンジンをエスケープキャラクタ終了文字の次の文字へ移動し、1.(もしくは3.)へ戻ります。このとき、元文字列が終わったら、解析終了です。おつかれさまでした。

以上が、文字列解析エンジンの動作手順です。簡単ですねw
これを見ると「元文字列は読むだけ」というのがわかったかと思います。元文字列は読むだけで、結果文字列のほうを切り貼りしているわけです。

3.文字列解析制御エスケープキャラクタ

これで文字列解析エンジンがどういう手順で動作しているかが理解できたかと思います。では、文字列解析制御エスケープキャラクタ(「||」)は、この動作のどこをどのように変化させるのでしょうか。

文字列解析制御エスケープキャラクタは、文字列解析エンジンの
  • カーソル位置の移動
  • 結果文字列
  • 「元文字列は読むだけ」というルール
の3つを変化させます。エスケープキャラクタの解析(記号をどのような文字列に展開・置換するか)自体は変化させません。これら3つのどれを変化させるかは、識別子(文字列解析制御エスケープキャラクタの||に囲まれた文字列)によって異なります。ちなみに識別子は大文字小文字は区別しません。

以下では、使用できる識別子ごとに、その変化のさせ方を見ていきましょう。


|label:|
この識別子は何もしませんw 後述する|skip:||rewind:||if:|等の目印となるだけのものです。
「…」の部分(ラベル名)には任意の文字列が記述できます。またこの文字列は大文字小文字は区別されません。

文字列解析エンジンの動作は、「エスケープキャラクタ終了文字(|)の次の文字へ移動」するだけで、結果文字列に何も追記しません。

|stop|, |end|, |terminate|
これらの識別子は、文字列解析をその位置で終了します。これより後方の元文字列は解析されません。

|clear|
この識別子は、現在の結果文字列を破棄し、カーソルをエスケープキャラクタ終了文字(|)の次の文字へ移動します。

|skip|
この識別子は、スタックからポップした値が正ならカーソルをエスケープキャラクタ終了文字の次の文字からその数値分だけ後方へ移動します。元文字列の紙テープをスキップするわけです。

スタックが空、もしくはスタックからポップした値がゼロまたは負なら、上図のゼロの位置(文字c)へカーソルが移動します(つまり何もしないのと同じ)。
スタックからポップした値が正の無限大(「inf@p@」または「+inf@p@」)だった場合、元文字列末尾へカーソルが移動します(解析が終了)。


|skip:|
この識別子は、「…」に記述された文字列と同じラベル名をもつ「|label:|」を後方に検索し、そこへカーソルを移動します
同じ名前のラベルが複数ある場合には、それらのうちで最も先頭のものに移動します。また、見つからなかった場合は、この識別子の次の文字へカーソルが移動します(つまり何もしないのと同じ)。

|label:|」を検索する方向が後方のみであることに注意してください。「|skip:|」の前方に同じラベル名の「|label:|」が存在しても、そこへはカーソル移動しません。

|skip|」と「|skip:|」は後方へしかカーソルを移動させませんので、これらを使用することで無限ループに陥る可能性は(これら単体では)かなり低いです。


|rewind|
この識別子は「|skip|」の逆で、スタックからポップした値が負ならカーソルをエスケープキャラクタ開始文字の前の文字からその数値分だけ前方へ移動します。要するに、元文字列の紙テープを巻き戻すわけです。

スタックが空、もしくはスタックからポップした値がゼロまたは正なら、このエスケープキャラクタ終了文字の次の文字(上図の文字'e')へカーソルが移動します(つまり何もしないのと同じ)。
スタックからポップした値が負の無限大(「-inf@p@」)だった場合、元文字列先頭へカーソルが移動します(解析を最初からやり直し)。

さて、上図の状態から、このまま解析を続けたらどうなるんでしょうか?
この場合ですと、'cd'が結果文字列に追記された後、同じ「|rewind|」が再実行されることになるんですが、先ほどの巻戻しでスタックが空になっていますので、2回目の「|rewind|」実行ではエスケープキャラクタ終了文字の次の文字(上図の文字'e')へカーソルが移動し、'ef'が結果文字列に追記され、解析が終了します。

|rewind|」は元文字列の巻戻しをおこなうので、同じ|rewind|が再実行されてしまうことになり、基本的に無限ループとなります。ただ、これはスタックを消費するので、上図のように運が良ければ、いずれスタックが空になり|rewind|の後方へカーソルが移動して解析が終了します。ですが巻き戻した箇所にスタックへプッシュするような操作が含まれていますと、スタックが空になりませんので無限ループすることになります。

|rewind:|
この識別子は「|skip:|」の逆で、「…」に記述された文字列と同じラベル名をもつ「|label:|」を前方に検索し、そこへカーソルを移動します。「|label:|」を目印に元文字列の紙テープ巻き戻しをおこなうわけです。
同じ名前のラベルが複数ある場合には、その最も先頭のものへ移動します。また、見つからなかった場合は、この識別子の次の文字へカーソルが移動します(つまり何もしないのと同じ)。

見ればわかる通り、この「|rewind:|」も無限ループになります。しかも悪いことに、これはスタックを消費しませんので、|rewind|であった『運良くスタックが空になったのでループを脱出する』が起こりえません。使えば確実に無限ループになります。
ですので、「|rewind:|」は後述する「|if:|」等の条件に応じてカーソル移動する識別子を用いてループを脱出できるように記述する必要があります。


|jump|
この識別子は「|skip|」と「|rewind|」を合わせたもので、スタックからポップした値が正ならカーソルをエスケープキャラクタ終了文字の次の文字からその数値分だけ後方へ、負ならカーソルをエスケープキャラクタ開始文字の前の文字からその数値分だけ前方へ移動します。ゼロの場合にはエスケープキャラクタ終了文字の次の文字へ移動します。

図は示しませんが、この「|jump|」も使い方によっては無限ループになります。

ちなみに、この識別子のラベル使用バージョンである「|jump:|」は実装されていません。理由は、「移動するラベル名がわかってるんなら、それが前か後ろかもわかっているはずなので、|skip:||rewind:|のどちらかで代替できるはず」だからです。


|if:|, |ifnot:|
これらの識別子は、スタックからポップした値の真偽(非ゼロか、ゼロか)によってカーソル移動するかどうかを判断します。「条件分岐」というやつですね。

|if:|」は、スタックからポップした値が真(ゼロでない)なら、「…」に記述された文字列と同じラベル名をもつ「|label:|」を前後どちらにも検索し、見つかったラベルのうち最も先頭のものへカーソルを移動します。偽ならエスケープキャラクタ終了文字の次の文字へ移動します。

|ifnot:|」は、スタックからポップした値が偽(ゼロ)なら、「…」に記述された文字列と同じラベル名をもつ「|label:|」を前後どちらにも検索し、見つかったラベルのうち最も先頭のものへカーソルを移動します。真ならエスケープキャラクタ終了文字の次の文字へ移動します。

|if:|」「|ifnot:|」では、スタックが空だった場合にはエラーと見做し、結果文字列を破棄して解析を終了し、この元文字列を含んでいたレイヤーの描画を中止します(強制終了はしません)。


|insert|
これは特殊な動作をする識別子で、「元文字列は読むだけ」というルールを変更する識別子です。|insert|」は、現在の結果文字列を切り取り、元文字列のエスケープキャラクタ終了文字から、スタックからポップした数値の文字数だけずらした位置に挿入し、エスケープキャラクタ終了文字の直後にカーソルを移動します。

スタックが空、もしくはスタックからポップした値がゼロまたは負なら、エスケープキャラクタ終了文字の直後に挿入します。

imagesフォルダー内に『$HH:mm$』と1行だけ書かれたテキストファイル(bar.txt)があった場合、「2@p@bar@0/-1/\F@|insert|abcd」という文字列は、以下の図のように解析されます。

この識別子は、レイヤーの設定項目に書かれていたオリジナルの文字列を変更するのではなく、文字列解析エンジンに現在セットされている「元文字列」を今回の解析に限り変更する、というものです。



 4.文字列解析の制御によるGlaejaプログラミング

これまでに解説してきた文字列解析制御エスケープキャラクタを使うことによって、文字列解析エンジンはいわゆるプログラミングの能力を得ることができました(多分、チューリング完全なんではないでしょうか)。

で、このプログラミング能力が、あなたのホームを素敵にカスタマイズするのにどう役に立つのかというと……わかりませんw 「こんなスゲェことができるようになるんだゼ!」という作例でも挙げられればよかったんでしょうが、私のセンスではどうにも思いつきません。

しかたないので、文字列解析制御エスケープキャラクタを使ったプログラミングの例を2つ3つ書いて、お茶を濁しましょうw

1.偶数・奇数の判別

まずは簡単なところで、「ある数が偶数なら"EVEN"、奇数なら"ODD"と表示する」というのをやってみましょう。

偶数・奇数の判別自体は、その数を2で割った余りがゼロか1かで判断すればイイんですが、"EVEN""ODD"の表示分けをどうすればイイんでしょうか。

例えば、文字列解析制御エスケープキャラクタを使わない従来通りの方法でやろうとするなら、
  • 2つの「マッチテキスト」レイヤーを重なるように配置
  • 片方(A)の表示文字列を透明色で「(数値)@p@2@p@mod@x@」とし、
    探索文字列を「0」、置換文字列を「EVEN」とする
  • 残り(B)の表示文字列を透明色で「(数値)@p@2@p@mod@x@」とし、
    探索文字列を「1」、置換文字列を「ODD」とする
といったところでしょうか。「マッチテキスト」レイヤーを2つ使うので"EVEN"・"ODD"の色分けが可能である、という利点がありますが、レイヤー2枚使うのはあまりスマートではありません。

そこで、文字列解析制御エスケープキャラクタを使って、「テキスト」レイヤー1枚でやってみましょう。
「ある数を2で割った余りがゼロなら"EVEN"、1なら"ODD"と表示」すればイイだけですので、

01(数値)@p@2@p@mod@x@|if:odd|EVEN|stop||label:odd|ODD

と記述できます。


2.ループの作り方

プログラム言語たるもの、「何らかの処理を繰り返しおこなう」くらいできて当たり前です。文字列解析制御エスケープキャラクタを使えば繰り返し処理も可能っちゃ可能なんですが、普通の言語にある「while」とか「for」のような便利な構造はありません。「|if:|」や「rewind:…」を使って頑張ってみましょうw

繰り返し処理には、「do~while(一旦処理をおこなって、最後に繰り返すか判断する)」と「while~(最初に処理をおこなうか判断して、しないなら繰り返し範囲外へ脱出する)」という2パターンがあります。

文字列解析制御エスケープキャラクタを使った記述ですと、前者は以下のように記述できます。

01…|label:begin|…(処理)(判断結果をスタックにプッシュ)|if:begin|…

また、後者はちょっと複雑ですが以下のように記述できます。

01…|label:begin|(判断結果をスタックにプッシュ)|if:break|…(処理)…|rewind:begin||label:break|…


3.FizzBuzz問題

「FizzBuzz問題」とは、『1から100までの全ての数について、その数が3の倍数なら"Fizz"と、5の倍数なら"Buzz"と、3と5の公倍数(つまり15の倍数)なら"FizzBuzz"と出力する』というもので、『コードがかけないプログラマ志願者を見分ける手法』として提唱されたものです(Wikipediaより)。

上記1.2.を組み合わせて、これを記述してみましょう。普通に書けばこんな感じですかね。

011@p@
02|label:begin|
03dup@0/3/\x@
0415@0/2/\p@mod@0/3/\x@
05|if:buzz|
06FizzBuzz, |skip:judge|
07|label:buzz|
08dup@0/3/\x@
095@0/1/\p@mod@0/3/\x@
00|if:fizz|
10Buzz, |skip:judge|
11|label:fizz|
12dup@0/3/\x@
133@0/1/\p@mod@0/3/\x@
14|if:normal|
15Fizz, |skip:judge|
16|label:normal|
17dup@0/3/\x@
18.0@0/2/\P@,
19|label:judge|
201@0/1/\p@+@0/1/\x@
21dup@0/3/\x@
22100@0/3/\p@>@0/1/\x@
23|ifnot:begin|

わかりやすいように分かち書きしてあります。実際に入力する場合には全てを1行にいれてください。

…アルゴリズムの説明は面倒なのでしませんが、どこか懐かしい感じのするスパゲティぐあいですねwww
 上記ループの中で「@p@」「@x@」という糖衣構文を使ってない点に注意してください。数値や"Fizz"などが結果文字列に書きこまれていますので、スタック演算に「@x@@0/-1/\x@)」を使うことで演算子が正しく解析されないのを防ぐためです。…面倒くさいですね。

実際に上記文字列を「マルチラインテキスト」レイヤーで解析・表示させたものが以下になります。

正しく処理されているのがわかるかと思います。
それと、実際にやってみるとわかるんですが、、、100回のループはめっちゃ重いですwww



……とりあえず、ここまでで終了。

8 件のコメント:

  1. 使わせていただいてます。

    偶数奇数の判別部分ですが、modeではなくmodeではないのでしょうか?

    時が偶数かつ分の前後半、時が奇数かつ分の前後半の四通りの判別で、時辰4刻を再現しようとしてるのですが、
    まず時の偶数奇数判定でつまずきましたorz
    $h$@p@2@p@mod@x@
    これで何も表示されないです。
    もしかしてスタックからポップの命令が必要でしょうか?

    返信削除
    返信
    1. > modeではなくmodeではないのでしょうか?

      ???

      > $h$@p@2@p@mod@x@
      > これで何も表示されないです。

      そりゃ何も表示されないです、結果はスタックに積まれますからね。

      > もしかしてスタックからポップの命令が必要でしょうか?

      そうです。この後に「.0@P@」等してスタックからポップして表示させてください。

      削除
    2. ああ、エントリ見なおしてやっと意味がわかりました。

      > modeではなくmodeではないのでしょうか?

      本エントリの該当部分を「mod」に修正しておきます。

      削除
  2. 先日、初めて使用させていただきました。
    わからないことがあるので、教えてください。
    SSIDを取得した際、接続先によって取得される文字数が変わるため
    想定している範囲からはみ出してしまうことがあります。
    先頭から10文字だけを表示させたいのですが
    この「先頭から10文字を取得する」という部分の考え方がわかりません。
    取得した文字数をカウントするようなことはできるのでしょうか。

    返信削除
    返信
    1. 文字列全体を「\r」で反転させてから「\D」で末尾10文字以外を削除して「\r」で反転させて戻す

      削除
  3. お返事ありがとうございます。
    なるほど、反転ですか。
    目から鱗でした。
    教えて頂いたやり方で、思うような表示ができました。
    ありがとうございました。

    返信削除
  4. 明けましておめでとうございます。いつもお世話になっております。
    文字列解析制御エスケープ||の使用について、よくわからないので教えてください。

    12|label:ab|34
    テキストレイヤーに上記を記述した場合、「1234」が期待値になるかと思いますが、上記のまま結果が返されます。
    何度読んでも記述のだめなところが分からなく……
    申し訳ありませんが、御指南いただけないでしょうか。

    返信削除
  5. 先ほどエスケープ||について質問させていただいたものです。
    申し訳ありません、自己解決しました。
    無視して頂いて構いません。
    これからも応援していますので頑張ってください。

    返信削除