AYA Version5 マニュアル †
このマニュアルは、HTML形式で配布されているものを、すぐに見られるようにWiki上に転記・一部わかりやすいよう加筆したものです。
AYA Version5 システム関数も参照してください。
概要 †
「あや」と読みます。文字列処理を行なうためのDLLです。
C言語に似た文法を使用して、
- 与えられた文字列を加工する
- プログラムした規則に拠って文字列を生成する
といった処理を行なうことが出来ます。
文はWindows DLLですので、直接利用するためには、プログラミングに関する知識がある程度必要です。
環境 †
Windows用です。
開発のごく初期に98SE、2000、xpで動作確認しましたが、最終版はxpでしか確認を取っていません。
報告をいただけると助かります。
利用規定 †
- 使途を限定せず自由に利用することが出来ます。
- 配布パッケージに含まれているすべてのファイルを自由に取り扱うことが出来ます。一部のファイル、もしくはそのファイルの内容の一部を抜き出して、他の構成に含めてもかまいません。
- このプログラムを使用した結果、あなたに何らかの損害が発生しても、その責任を作者は負いません。
- ソースコードを改変して自由に異版を作成し、配布することが出来ます。
その際、配布者はその意志に基づいたライセンスを独自に策定し配布物に適用することが出来ます。
主な変更点 †
version 4系列の文をお使いの方は、この項を読むことでversion 5の性能のあらましを簡単に理解できます。
新機能/改善された機能 †
- UTF-8に対応
version 4で扱える文字コードはShift_JISのみでしたが、version 5ではShift_JISに加えてUTF-8、およびOSデフォルトの文字コードが使用可能になりました。
- 数式書式の制限撤廃
関数の引数に数式を記述できない、代入演算子や比較演算子を式に包含できない等の制限がなくなり、自由に式を記述できるようになりました。
- if等の真偽判定が0/非0判定へ
version 4ではif等の判定式のために特別の書式が必要でした。version 5にはそのようなものは無く、単に値で判定できます。
- 汎用配列機能を追加
型を混在して格納可能な配列構造が追加になりました。~文字列を格納する場合でも簡易配列より高速に動作します。
- 範囲付き埋め込み展開
"hello, %username" を "hello, %(username)" のように書けます。このように( )で囲むと動作速度が向上します。また、( )内はいわゆるeval風に動作するため、たとえば代入式などを文字列内に埋めることが可能になっています。
- 各種システム関数追加
正規表現、ランダム選択、書式化出力など、従来不可能だった動作を実現する数々の関数が追加されています。
- foreach
簡易配列、汎用配列に対してforeachが使えます。
- 動作速度の向上
実行するスクリプトに強く依存しますが、ループや変数展開などのベンチマーク実測においてversion 4比2~3倍程度という値が出ています。
- version 4が抱える問題の解決
version 4はパーサがぬるく、これに起因する種々の不安定動作が大小さまざまな不具合を引き起していました。
version 5ではそのような不安定要因は極力排除されています。
- エラーログ改善
ログに記録されえるエラーの内容が詳細化されました。エラー時は該当する辞書ファイル名と行番号が記録されます。
変更された機能 †
version 5の開発においては、version 4が抱える不具合や仕様の不整合を矯正することが重視されました。
互換性の確保にも配慮が無かったわけではありませんが、それが足を引っ張るようならきっぱり切り捨てる方針で臨みました。
以下の点で互換性が崩れています。移植の際には注意が必要です。
- 上位/下位インタフェース変更
重要で本質的な変更です。version 4ではHTTP風リクエストヘッダ文字列を送受する構造を前提としていましたが、version 5は特別な書式が決められていない文字列をやりとりするだけです。
- システム関数全面見直し
version 4が持つシステム関数のうち、完全に同一機能のまま引き継がれたものは半数程度です。多くが新規追加、廃止、あるいは機能変更されています。詳細はシステム関数リファレンスで確認して下さい。
最も注意を要するのは文字列処理系の関数です。
version 4 STR*系(バイト数単位)、MSTR*系(文字数単位)の二系統が存在
version 5 STR*系(文字数単位)
STR*系関数が残されましたが、動作はMSTR*系のものになっています。これは内部処理がUNICODE化されたためです。
- システム変数全廃
_argv、_argc以外のシステム変数は廃止されました。ただし同等機能を持つシステム関数が追加されています。
- 空の文字列
version 4では空の文字列と値無しの区別が不明確でした。version 5ではこれが厳格に区別されます。
- 関数/変数名の先頭に数値を使えない
version 4では使用可能でした(そもそも12hourというシステム変数があったほどです)。これはversion 5では禁止となりました。
他にも変数名/関数名として使用不可能になった文字が存在します。
- 機能制限設定機能は未搭載
考えた結果、とりあえず未実装としました。
- そのほか...
version 4のパーサが弱かったおかげで見逃されていたエラーが顕在化する可能性があります。
エクスポートしている関数 †
文は以下の公開された関数を持っています。
文を利用するプログラムは、文をLoadLibraryした後にこれらの関数を実行して所望の処理を行ないます。
extern "C" __declspec(dllexport) BOOL __cdecl load(HGLOBAL h, long len)
文に初期化を指示します。
文をLoadLibraryして使用を開始する直前に、この関数を一度だけ必ず実行してください。
hには「文がカレントとして認識するディレクトリ絶対パス」を、lenはhの長さを渡してください。
hの領域開放は文側で行ないますので、呼び出し側では使い放しでかまいません。
extern "C" __declspec(dllexport) BOOL __cdecl unload()
文に終了を指示します。
文をFreeLibraryする直前に一度だけ実行してください。
extern "C" __declspec(dllexport) HGLOBAL __cdecl request(HGLOBAL h, long *len)
文に処理を指示し、結果を得ます。
hには処理対象の文字列を、*lenにhの長さを渡してください。
渡したhの領域開放は文側で行ないますので、呼び出し側では使い放しでかまいません。
処理結果は戻り値で得られます。処理結果の長さは*lenに格納されています(つまりこの値は書き換えられます)。
戻り値を取得した後、領域を*lenで示されるサイズで開放(GlobalFree)してください。
なお、これはデスクトップマスコットソフトウェア「伺か」で使用される擬似AI用DLL「SHIORI」
のインタフェース規格と完全に同一のものです。
基礎設定 †
文 ver.5のデフォルトのDLLファイル名は「aya5.dll」です。
主ファイル名「aya5」は他の名前に自由に変更することができます。
文を動作させるためには「基礎設定ファイル」と呼ばれるファイルが必ず必要となります。
基礎設定ファイルのファイル名は「主ファイル名.txt」です。すなわちデフォルトでは「aya5.txt」となります。もしあなたがDLLのファイル名を
「hoge.dll」に変更したなら、基礎設定ファイルは「hoge.txt」です。
基礎設定ファイルはテキストファイルで、実行されるOSのデフォルトの文字コードで解読されます。
もし国際化に関して考慮しなければならないなら、マルチバイト文字コードに関する問題を回避するため、基礎設定ファイルはASCIIコードのみで記述するべきです。
以下に例を示します。
// dics
dic, basis.dic
dic, control./*doc*/ayc
// option parameters
charset, UTF-8
msglang, english
log, executelog.txt
iolog, off
fncdepth, 16
設定はコマンドとパラメータをカンマで区切って指定します。
空行(改行のみの行)、"//"以降、および"/*"と"*/"で囲まれた領域はコメントと見なされます。
コマンドとその意味は以下の通りです。
charset, name †
標準の文字コードを設定します。
以下の予約値からいずれかを指定します。指定が無い場合はShift_JISとして扱われます。
- Shift_JIS / ShiftJIS / SJIS
シフトJISコード。
- UTF-8
UTF-8。
- default
実行OSのデフォルト文字コード。
dic, filename †
辞書ファイルfilenameをロードします。
辞書ファイルは文スクリプトが記述されたプログラムソースファイルで、文はここのプログラムされた内容を元に動作します。
辞書ファイルはいくつでも指定し、読み込むことができます。
filenameはloadで指定されたパス位置からの相対パスで指定します。
標準の辞書ファイルはcharsetで指定した文字コードで記述されたプレーンテキストファイルです。この他に、一定の法則でスクランブルをかけた
暗号化ファイルを読み込むことができます。
msglang, language †
ログに記録されるエラーメッセージ類の言語を選択します。
以下の予約値からいずれかを指定します。指定が無い場合はjapaneseとして扱われます。
- japanese
日本語。
- english
英語。
log, logfilename †
実行ログをファイルlogfilenameに記録します。
charsetで指定した文字コードで書き込まれます。
logfilenameはloadで指定されたパス位置からの相対パスで指定します。
iolog, [on|off] †
load、unload、request実行時の入出力文字列と処理時間をログに記録するかを設定します。
onで記録します。デフォルトではonになっていますので、不要の場合にoffとしてください。
fncdepth, depth †
関数呼び出しの深さ上限を数値で指定します。
デフォルト値は32です。最低値は2で、これより小さな値や不正な値を指定した場合も2として扱われます。
文スクリプトリファレンス †
文スクリプトの書式はC言語のそれを踏襲しています。
したがってあらかじめC言語、およびC言語に類似する言語を習得していると理解が早いです。
関数 †
基礎 †
request実行時に"Hello World"という文字列を返すプログラムコードの全体を以下に示します。
request
{
"Hello World"
}
文をロードしたモジュールがHGLOBAL request(HGLOBAL h, long *len)を実行すると、このスクリプトが実行され、"Hello World"が
返されます。
loadとunloadも同様です。
ただしloadとunloadは値を返さないので、出力文字列を書いても意味がありません。loadには変数に初期値を入れるといった初期化処理を、
unloadには逆にその後始末をするコードを書きます。
必要な関数だけ書けばよいです。不要なものは省けます。
たとえば上の例ではloadとunloadは書いていませんが、これはエラーとはなりません。何もしないだけです。
極端な例を挙げるなら、辞書ファイルがまったく無くてもエラーにはなりません。この場合loadとunloadは何もせず、
requestは空の文字列を返します。
loadとrequestはひとつの引数を持っています。これは変数で取り出せます。
変数の名前は_argcと_argvで、これはC言語のmain関数のインタフェースと類似しています。
_argc †
引数の数。loadとrequestでは引数はひとつの文字列ですから1となります。unloadは引数を持たないので、この値は0となります。
_argv †
引数の実体が格納されています。これは_argc個の要素を持つ配列です。各要素へのアクセスは演算子[i]で行います。
序数iは0オリジン指定です。たとえば_argcが1の場合は_argv[0]が使用可能で、ここに引数が格納されていることになります。
基礎編のまとめとして、loadでは変数strに"Hello"を格納し、requestで引数として渡された文字列とstrを結合して返すプログラムコードを
示します。~これまでの説明を踏まえて読んでみてください。
load
{
str = "Hello"
}
request
{
str + " " + _argv[0] + "!"
}
処理対象文字列を"World"としてrequestを実行すると、結果として"Hello World!"が得られます。
書式 †
以下の則があります。
- 空行(改行のみの行)、"//"以降、および"/*"と"*/"で囲まれた領域はコメントと見なされます。
- 1行の終わりがスラッシュ("/")の場合は、次の行がこの行の後ろに結合されます。
- 行頭、および単語間には自由に空白文字を入れることが出来ます。空白文字は空白、およびタブ文字です。
- 複数のステートメントを1行に詰めて書きたい場合は、セミコロン(";")で区切ることが出来ます。
つまり先に挙げたHello Worldコードは以下のように1行に詰めて書くことが出来ます。
request{"Hello World"}
では以下のように書いてもいいのか? もちろん。問題なく動作します。(ただしこんな風に書くのは感心できない!)
req/
uest {
"/
Hello World" }
/の次の行(新たに結合される行)先頭にある空白文字はインデント文字と見なされ、消されます。
したがって以下の文字列の結合結果は "ABCDEFG" であり、決して "ABCD EFG" ではありません。
"ABCD
EFG"
第4項を説明します。
以下のrequestは1+2の答えを求め、日本語の文章にして返しています。
request
{
answer = 1 + 2
"答えは" + answer + "です。"
}
セミコロンを使用して以下のように書けます。
request
{
answer = 1 + 2; "答えは" + answer + "です。"
}
セミコロンは過剰に書いても問題ありません。それらは無視され、動作に影響は与えません。
したがって、あなたがC言語の書式に慣れているなら、各行の終端に必ずセミコロンを書くことができます。
ユーザー関数の定義と実行 †
load、unload、request以外に好きな名前の関数を作成できます。
作成した関数は、その名前を書くだけで実行できます。
request
{
hello
}
hello
{
"Hello World"
}
上に示したのはもっとも単純な例です。requestは結果として"Hello World"を返します。
同じ結果が得られるもう少し複雑な例を、以下に示します。
request
{
combine("Hello", "World")
}
combine
{
_argv[0] + " " + _argv[1]
}
関数名の後ろに( )をつけて、その中にカンマで値を並べて書くと、これらは該関数に渡される引数として扱われます。
すなわち変数_argvと_argcに値が格納されて、該関数内で参照できるようになります。
ここで挙げた例では、combine関数内において_argcは2、_argv[0]は"Hello"、_argv[1]は"World"となるわけです。
関数名は自由につけられますが、以下に抵触する名前はエラーとなります。
- 数字0~9で始まる。
- アンダースコア("_")で始まる。
- アンダースコアで始まる名前はローカル変数で使われています。
- 以下の文字を含む。
空白 ! " # $ % & ( ) * + , - / : ; < = > ? @ [ ] ` { | }
- アンダースコア以外の記号は使わないほうが良いでしょう。
- 予約語と完全に一致する。
- アルファベット大文字+アンダースコアはシステム関数で使われます。
- forやwhile、ifなど、制御構造に使われるものも避けなければいけません。
関数は再帰呼び出しが可能です。
もっともありふれた例として階乗計算を行った例を挙げます。
request
{
factorial(5)
}
factorial
{
if !_argv[0]
1
else
factorial(_argv[0] - 1)*_argv[0]
}
requestは120を返します。
択一 †
request
{
"Hello World"
"こんにちは世界"
"Hallo Welt"
}
このように列挙すると、これらは平等な「出力候補」として扱われ、出力はこれらのうちのいずれかひとつになります。
5種類の選択方法が用意されており、いずれかを選ぶことができます。~ただし、voidとarrayは特殊な用途のみで使用します。
なにもなし †
デフォルトでは無作為に選択します。
nonoverlap †
すべての候補が出力されるまでは、同じ候補を選択しなくなります。
request : nonoverlap
{
"Hello World"
"こんにちは世界"
"Hallo Welt"
}
上のように、関数名の後ろに": nonoverlap"を付け加えます。
sequential †
記述された順に出力します。最後まで出力したらまた先頭に戻ります。
request : sequential
{
"Hello World"
"こんにちは世界"
"Hallo Welt"
}
上のように、関数名の後ろに": sequential"を付け加えます。
void †
何も出力しなくなります。
以下の例の場合、requestは3つの候補のどれも出力しません。
request : void
{
"Hello World"
"こんにちは世界"
"Hallo Welt"
}
「何も実行しない」ではなく、「何も出力しない」であることに注意してください。
内部に書かれた関数や数式は処理されます。
increment_i : void
{
i++
i
}
上の関数increment_iはiに1を加算します。voidが無い場合、この関数は加算結果を返しますが、voidを指定すると値を返さず、
ただ加算を行うのみとなります。
array †
出力候補をすべて結合した汎用配列を関数の返値とします。
request : array
{
"This is a pen."
("A","B","C")
3.14
}
出力は汎用配列 ("This is a pen.", "A", "B", "C", 3.14) となります。
nonoverlapとsequentialは、出力確定子が存在する場合でも取り得るすべての組み合わせに対して正常に機能します。
たとえばsequentialを例に挙げると、
request : sequential
{
"1"
"2"
"3"
--
"A"
"B"
}
requestは以下の順序で出力を発生します。
"1A" "2A" "3A" "1B" "2B" "3B" "1A" "2A" …
関数によっては実行毎に出力候補の数が変動します。~たとえば以下の関数は、変数iの値によって候補数は2個もしくは4個に変化します。
request : sequential
{
if i {
"1"
"2"
}
"3"
"4"
}
候補数が変化した場合、nonoverlapとsequentialの巡回順序は初期化され、最初からやり直しになります。この時に限って、前回と同じ値が重複して
出力されることはあり得ます。
入れ子 †
request
{
{
"Hello World"
"こんにちは世界"
}
"Hallo Welt"
}
{ } は階層的にいくつでも重ねて書くことが出来ます。動きは最上層の{ }と同じです。すなわち、包含される候補からひとつを選択して出力します。
ただし選択方法は無作為抽出のみで、最上層のようにnonoverlapやsequentialの指定はできません。
上の例では、{ }の有無にかかわらず結果は同じであるように思われるかもしれませんが、実はそうではありません。
{ }が無い場合、それぞれが出力される確率は平等に1/3です。しかし上の例の場合、まず"Hello World"と"こんにちは世界"からひとつが選ばれ、次いでその結果と残った"Hallo Welt"から出力が抽出されます。すなわち、出現率は"Hello World"と"こんにちは世界"が1/4、"Hallo Welt"は1/2となるのです。
出力確定子 †
request
{
"Hello"
"Perfect"
"Peaceful"
--
" Wor"
--
"ld"
"th"
}
nonoverlap、sequentialと組み合わせて使用した場合、(グループ単位ではなく)関数が取り得る全ての組み合わせに対して動作します。
出力確定子はどこでも問題なく使用できます。{ }入れ子の深い位置でも使えます。
文は文字列のほかに数値なども扱えますが、出力確定子は結合する際にそれらをすべて文字列に変換し、文字列として結合します。
値と変数 †
即値 †
文が扱える値は整数、実数、文字列の3種類です。
整数 †
符号付き32bit整数です。
10進数値はそのまま記述します。
先頭に"0b"を付加することにより、2進数値を記述できます。
先頭に"0x"を付加することにより、16進数値を記述できます。
以下の関数int10は整数10を返します。3つの記述はいずれも10進数では10だからです。
int10
{
10
0b1010
0xa
}
実数 †
符号付き64bit浮動小数点数です。小数点以下の桁がある場合や、精度が落ちてもいいので非常に巨大な数値を扱いたい場合はこれを使用します。
整数との区別は小数点の有無です。文は数値に小数点を見つけると、これを実数として取り扱います。
文字列 †
ダブルクォート(")で囲まれた値は文字列です。
文字列の中にダブルクォートが含まれてはいけません。
文字列(展開なし) †
シングルクォート(')で囲まれた値は、展開されない単純な文字列です。
文では文字列中に変数や関数を埋め込むことが出来ますが、これが展開されるのはダブルクォートで囲まれた文字列のみです。
文字列の中にシングルクォートが含まれてはいけません。
変数 †
変数は値を格納するための領域です。
ひとつの変数には、
- 整数、実数、文字列
- 要素ごとに上記のいずれかを格納可能な汎用配列
を格納することができます。
名前は以下の禁止条項に抵触しない限りは自由につけることができます。
- 数字0~9で始まる。
- 以下の文字を含む。
空白 ! " # $ % & ( ) * + , - / : ; < = > ? @ [ ] ` { | }
- 予約語と完全に一致する。
- 関数名と完全に一致する。
値の格納(代入)は代入演算子 = で行ないます。内容を出力するには、関数と同様にその名前を書きます。
request
{
str = "こんにちは"
str
}
上は単純な例で、変数strに文字列を格納し、そのまま出力しています。
まだ値が代入されていない変数は、空の文字列を出力します。~「何も出力しない」ではないことに注意してください。
request
{
"Hello World"
i
}
i は存在しません。これは空の文字列になりますから、結局上の例は下のコードと等価であり、"Hello World"もしくは空の文字列を1/2の確率で出力することになります。
request
{
"Hello World"
""
}
変数のスコープと寿命 †
変数はスコープ(有効範囲)の違いによって2種類存在します。
- グローバル変数
すべての関数で共通に使用できる変数。寿命は永遠。
- ローカル変数
現在の{ }内、およびそれより深い入れ子階層のみで使用できる変数。寿命は該当する{ }を抜けるところまで。
両者を区別するのは変数の名前です。変数名の先頭がアンダースコア("_")の変数は、ローカル変数として扱われます。
「必要な範囲だけで有効な変数」であるローカル変数をうまく利用することで、プログラムの見通しが良くなります。
request
{
_i = "3*2は"
_j = multi(3)
_i +_j + "です"
}
multi
{
_i = _argv[0]
_i * 2
}
requestとmultiが同じ名前の変数 _i を使っていますが、それらはまったく別物として扱われます。お互いの値を破壊することはありません。
関数の引数を扱う変数_argcと_argvも実はローカル変数であることに気付かれたかと思います。~これらもまた関数ごとに別の値を格納しますので、
ローカル変数になっているわけです。
ローカル変数が「現在の関数内で使用可能な変数」ではなく、「現在の{ }内、およびそれより深い入れ子階層のみで使用できる変数」であることには注意してください。
request
{
{
_str = "Hello World"
}
_str
}
このプログラムは期待通りには動作しません。
_strは{ }内のみで有効ですから、出力を取り出そうとしている位置では消えてしまっています。
したがって、この関数requestは空の文字列を出力します。
グローバル変数とローカル変数の違いはもうひとつあります。寿命です。
ローカル変数は上で見たとおり、変数を使用している{ }から外れると消えてしまいます。
対してグローバル変数はどこでも使用可能、さらにunloadでその値はファイルへ自動的に保存され、loadで復元されるようになっています。すなわちグローバル変数の内容は、(特殊な操作によって意図的に消去しない限りは)永遠に保持されるのです。
演算 †
基本 †
C言語と同様の書式で四則演算、比較演算、代入、その他が可能です。
演算の順序は演算子毎に重み付けされた演算優先度に基づいて決定されます。また、ブラケット( )で囲まれた部分は最優先で演算されます。
演算子の種類と演算優先度は以下の通りです。
演算子 意味 優先度
( ) [ ] ブラケット 高い
! 否定
++ -- ポストインクリ/デクリ
* / % 乗除算、剰余
+ - 加減算
& フィードバック
== != >= <= > < _in_ !_in_ 比較
&& 論理積
|| 論理和
= := 代入
+= -= *= /= %= +:= -:= *:= /:= %:= ,= 演算して代入
, 汎用配列要素の列挙 低い
コロン(":")付きの代入演算子は過去互換性を保持するために残されているもので、機能的にはコロン無しのものと同等です。
*は単項演算子です。
ブラケット( )については次項で詳説します。
カンマ演算子(",")、およびスクウェアブラケット[ ]については配列の項で詳説しています。
フィードバック演算子&については別項で詳説しています。
_in_と!_in_は文字列の包含チェックに使用する演算子です。
foo
{
"or" _in_ "World"
}
_in_は左辺の文字列が右辺の文字列に含まれていれば1、含まれていなければ0を返します。!_in_はその逆です。
上の関数fooは1を返します。
比較演算子は結果が真であれば整数1を、偽であれば0を返します。
これらの演算子は文字列に対しても正しく機能します。値の大小は辞書順比較で決定されます。
論理の真偽は以下で判断されます。
偽 整数0、実数0.0、空の文字列、空の汎用配列
真 上記以外のすべて
代入でない演算は結果がそのまま出力となります。
foo
{
(3+2)*4
}
この関数fooは20を出力します。
同じ優先度の演算子が連続している場合は、常に左から結合されます。
1+2-3
たとえば上の式は
- 1+2
- 3-3
の順に計算されます。
i = j = 10
これはどうなるでしょう。C言語では代入演算子は右から結合されますから、iとjには10が代入されます。
しかし文では結合は常に左からです。すなわち
- i = j
- j = 10
の順に計算されます。したがって、iには10は入らないのです。
i = (j = 10)
これで i にも10が入るようになります。
演算対象の項の型が一致していない場合、結果の型は以下のようになります。
- 整数と実数の演算
結果は実数となります。
- 整数/実数と文字列の加算
数値は文字列に変換された後、もう一方の文字列に結合されます。
- 整数/実数と文字列の演算(加算以外)
演算不能ですがエラーとはなりません。結果は空の文字列となります。
ひとつの演算式内で型が混在していても構いません。
必要に応じて、上記の法則により型変換が起こります。
"10+2は" + (10+2) + "です。"
最初に10+2が整数として計算され、結果12が得られます。残りはすべて文字列との加算ですから、12は文字列に変換され、文字列として結合されます。
ブラケット( )による演算順序制御 †
ブラケット( )で囲んだ部分は演算順序が最優先扱いになります。
( ) はいくつでも重ねて指定可能で、深くなるほど優先度が上がります。
平たく言えば「かっこで囲んだところを先に計算する」ということです。これは当たり前の規則なのでいまさら大きく扱うまでもないのですが、
数式の書き方によってはなかなかややこしいこともあります。
以下の例を見てください。
answer = (_i = 10) + (2*(_i + 10))
文の演算則をよく理解している人でないと、answerに何が代入されるかを正確に答えることは出来ないでしょう。
answerには文字列の"10"が入ります。決して整数50ではありません。
最初に計算されるのはどこでしょう。ブラケットが一番深いところ、すなわち _i + 10 です。さて変数 _i はまだありません。したがってこれは空の文字列になります。となると、_i + 10 は文字列と整数の加算です。10は文字列に変換され、"10"となります。しかし次が整数との乗算なので、結局また空文字列になってしまいます。
ここまで説明すれば結果が文字列の"10"になる理由は明白でしょう。
さて、こちらの意図ではまず_i = 10の代入を先にしてもらいたいわけです。
このような場合、文では以下のようにブラケットを過剰に付与することで演算順序をコントロールします。
answer = (((_i = 10))) + (2*(_i + 10))
これで代入部分の優先度が一気に引き上げられました。今度は正しい結果が入るはずです。
なお、代入部分を囲むブラケットは2段で十分ではないかと思われるかも知れませんが、それも誤りです。ブラケット2段は_i + 10と同じ深さとなりますが、=と+では優先度が+のほうが高いのです。
フィードバック演算子& †
フィードバック演算子&は、他の演算子とは使い方がまったく異なる独特の演算子です。
request
{
_i = 1
foo(&_i)
}
foo
{
_argv[0] = 100
}
関数呼び出しの際に引数として変数を与える場合、その先頭に&を書くことができます。
先頭に&を付けられた変数は、呼び出し先関数の_argvの対応する要素へ関連付けられます。すなわち、_argvを書き換えると、対応する呼び出し元の変数の値も書き換わるようになります。
上の例では、fooを実行すると _i の値が100に書き換わります。
フィードバック演算子は好きな位置にいくつでも使用可能です。
request
{
foo(1, 2, &_value, "Hello", &_value2)
_value + _value2
}
foo
{
_argv[2] = _argv[0] + _argv[1]
_argv[4] = _argv[3] + " World"
}
関数requestは "3Hello World" を出力します。
当然のことですが、フィードバック演算子は変数にしか付けられません。
配列 †
演算子[ ]は配列要素にアクセスするための演算子です。
配列には文字列スプリットを擬似的に配列と見なす「簡易配列」と、カンマ区切りで列挙された要素を扱う「汎用配列」の2種類があります。
簡易配列 †
文字列に含まれるカンマをデリミタ(区切り文字)と解釈して、配列のように扱う機能です。
request
{
_a = "this,is,a,pen"
_a[1]
}
requestは"is"を出力します。
[ ]演算子が処理する対象は変数である必要はありません。即値でも関数の返値でもいいです。
上の例は以下のように書いても同じです。
request
{
"this,is,a,pen"[1]
}
序数の後ろにカンマ区切りでデリミタを指定することにより、カンマでなく他の文字列で簡易配列要素を区切ることができます。
request
{
"This is a island."[2,"is"]
}
"is"で区切るわけですから、文字列は以下のように分解されます。
[0] "Th"~
[1] " "~
[2] " a "~
[3] "land."~
したがってrequestは" a "を返します。
デリミタ指定をうまく使うと、多次元配列風に値を取り出すことが出来ます。
request
{
_ar = "taro|male,ayame|female,hotaru|female"
_ar[2][1,"|"]
}
_ar[2]は"hotaru|female"です。さらに"|"を区切り文字として解釈して[1]を取り出しますので、結果は"female"となります。
このように、階層毎にユニークなデリミタを適用することによって、任意の位置の値を簡単に抜き出すことができるようになります。
範囲外の序数を指定した場合の出力は、存在しない変数の値を取り出そうとした場合と同様、空の文字列となります。
ここからは変数限定の機能を解説します。
要素に代入が可能です。
request
{
_a = "this,is,a,pen"
_a[3] = "eraser"
_a
}
"pen"を"eraser"に書き換えています。requestの実行結果は"this,is,a,eraser"となります。
デリミタ指定しても正しく機能します。
request
{
_s = "This is a island."
_s[2,"is"] = " beautiful "
_s
}
requestは"This is beautiful island."を出力します。
多次元配列風に[ ]演算子を連結して使用している場合、代入はできません。
request
{
_ar = "taro|male,ayame|female,hotaru|female"
_ar[1][1,"|"] = "male"
}
ayameの性別をmaleに書き換えようとしていますが、この操作はエラーとなります。代入が可能なのは一次元の場合のみです。
現在の要素数を越える位置へも問題なく代入できます。デリミタが自動的に追加され、要素数が拡張されます。
request
{
_m = "fuji/asama/tanigawa"
_m[5,"/"] = "daisen"
_m
}
requestは"fuji/asama/tanigawa///daisen"を出力します。
SETDELIMという関数を使用すると、「デフォルトのデリミタ」をカンマから別の文字列へ変更できます。
先に示した例をSETDELIMを使用して書き換えたものを以下に示します。
request
{
_m = "fuji/asama/tanigawa"
SETDELIM(_m, "/")
_m[5] = "daisen"
_m
}
SETDELIMすることにより、単純に_m[5]と書くことができるようになります。
多次元配列風に[ ]演算子を連結して使用している場合、SETDELIMは最初の(一次元目の)[ ]のみで有効です。
汎用配列 †
汎用配列はさまざまな型の値を混在して格納できる配列構造です。
一般的なアクセスでは簡易配列より高速に動作します。
初期化 †
i = (100,"test",-1.5)
要素をカンマで列挙して記述します。
代入する場合は上のように要素の集合を( )で囲んでください。カンマの演算優先度は代入よりも低いため、ブラケットが無いと
(i = 100),"test",-1.5
このように解釈されてしまいます。
配列を空の状態で初期化するには、IARRAYという関数を使用します。IARRAYは「空の汎用配列」を返す関数です。
i = IARRAY
初期化時に要素をひとつだけ代入したい場合は工夫が要ります。たとえば単に i = 100 としたのでは、配列ではなくただの数値の代入になってしまうからです。
以下のように記述します。
i = (IARRAY,100)
要素追加 †
i = (i,"add")
とすると配列の後端に"add"が追加されます。
配列を追加することもできます。
i = (i,("add",123,0.0))
a = a + 1 を a += 1と略せるように、上の例は下のようにも書くことができます。
i ,= ("add",123,0.0)
先頭への挿入も同じ書式で可能です。
i = ("first",i)
中途への挿入もできます。
i = (100,200,300,400,500,600)
i[2] ,= "insertion"
iは(100,200,300,"insertion",400,500,600)となります。
挿入対象は[2]でなく[3]に入ることに注意してください。
i[2]へ挿入したい場合は
i[2] = ("insertion",i[2])
とします。
要素削除 †
削除したい要素へIARRAYを代入します。
i = (100,200,300,400,500,600)
i[2] = IARRAY
300が削除され、iは(100,200,400,500,600)となります。
値の更新 †
単純に要素へ代入できます。
現在の要素数を越える位置へも問題なく代入できます。必要な数だけ要素数が拡張されます。
値の取り出し †
通常の変数と同様、序数を指定して記述すればそのまま出力されます。
範囲外の序数を指定した場合の出力は、存在しない変数の値を取り出そうとした場合と同様、空の文字列となります。
i = (100,200,300,400,500,600)
i[4]
500が出力されます。
[ ]演算子が処理する対象は変数である必要はありません。即値でも関数の返値でもいいです。
(100,200,300,400,500,600)[4]
500が出力されます。
汎用配列をそのまま関数の出力にできます。
request
{
river[2]
}
river
{
"tenryu","bandou-tarou","ishikari","shimanto"
}
requestは"ishikari"を出力します。
多次元化はできない †
汎用配列は多次元配列を構成できません。
(100,200,(300,400),500,600)
このように書いても、内包されたブラケット部分が副次的な配列と認識されることはありません。
結局以下のように単純に結合されてしまいます。
(100,200,300,400,500,600)
演算 †
要素単位の演算は通常に行うことが出来ます。
汎用配列と単項値との演算は、「全要素へ単項値が演算される」という独特の動作となります。
pref = "gunnma","ohsaka","hokkaido"
pref += "-ken"
answer = (2*(1,2,3))[1]
prefは "gunnma-ken","ohsaka-ken","hokkaido-ken" となります。
answerには4が代入されます。
2*(1,2,3)の計算結果が(2,4,6)となるからです。
関数の引数 †
文において関数の引数は汎用配列です。_argvの内容は、呼び出し側引数がそのまま代入された汎用配列となっています。
つまり
func(1, 2, "test")
という関数の呼び出しは、
_i = (1,2,"test")
func(_i)
とも書けます。非常にトリッキーな表記ですが、これで意図どおりに動作します。
注意してください。書き直したスクリプトにおいても、引数の数は決して1個ではありません。3個です!
この構造をうまく利用すると、可変長の引数を他の関数へ簡単に渡すことが出来ます。
request
{
total(1,2,3,4,5,6)
}
total
{
calc_total(_argv)
}
calc_total
{
_answer = 0;
foreach _argv; _val { _answer += _val }
_answer
}
totalは自分では何もせず、すべての引数をそのままcalc_totalへ引き渡しています。
この例では単純に引き渡していますが、もちろん必要に応じて加工してから渡すことも可能です。
引数の指定方法が複雑になっている場合は注意すべきです。
_i = (1,2,"test")
func("sky", _i, "sun")
上の呼び出しは以下と等価です。汎用配列は多次元化できないことを思い出してください。
func("sky", 1, 2, "test", "sun")
デリミタ/取得数指定 †
_i = (2,"is")
"This is a island."[_i]
簡易配列のデリミタ指定の部分も汎用配列です。したがって上のようなことも出来ます。これは下の記述と等価です。
"This is a island."[2,"is"]
範囲指定 †
簡易配列/汎用配列とも、序数を範囲で指定できます。取得、代入とも可能です。
範囲は汎用配列で指定します。たとえば i[a,b] は「iの要素a~b」を表します。
name = ("さくら","せりこ","奈留","まゆら","毒子","美耳")
i = name[1,3]
name[3,4] = "奎子"
j = name
name[0,2] = IARRAY
k = name
iには ("せりこ","奈留","まゆら") が格納されます。
jは ("さくら","せりこ","奈留","奎子","美耳") 、
kは ("奎子","美耳") となります。
範囲外は無視されます。
n = (1,2,3,4)
n[-2,1] *= 5
nは (5,10,3,4) です。
対象が簡易配列の場合でも書式は同じです。
name = "さくら,せりこ,奈留,まゆら,毒子,美耳"
i = name[1,3]
name[3,4] = "奎子"
j = name
iは "せりこ,奈留,まゆら" 、jは "さくら,せりこ,奈留,奎子,美耳" です。
デリミタ指定も可能です。範囲指定のすぐ後ろに続けて指定します。
animal = "くま!うさぎ!ねこ!いぬ!わに"
i = animal[0,2,"!"]
animal[2,4,"!"] = "ぶた"
j = animal
iは "くま!うさぎ!ねこ" 、jは "くま!うさぎ!ぶた" です。
汎用配列のパラレル出力 †
すべての式/値の前に「parallel」を書くことができます。
parallelは出力候補値が汎用配列だった場合、
要素のそれぞれを出力候補値にする機能を持っています。
foo0
{
("A","B","C")
"地球"
}
foo1
{
parallel ("A","B","C")
"地球"
}
foo0の出力は、("A", "B", "C") もしくは "地球"。
foo1の出力は、"A"、"B"、"C"、"地球" のいずれかとなります。
汎用配列以外にparallelを使っても意味がなく、書かないのと同じです。たとえば下の2つの記述は等価です。
parallel STRLEN("earth")
STRLEN("earth")
択一メソッドarrayとparallelを利用することにより、関数の出力候補と汎用配列を相互に変換することができます。
さまざまな応用が考えられます。たとえば以下の非常に簡潔な関数cyclicは、汎用配列から記述順に要素値を取り出します。
cyclic : sequential
{
parallel _argv
}
文字列内埋め込み要素の展開 †
文字列の中に変数や関数を埋め込んで、これらの実行結果を当該位置へ挿入することができます。
範囲付き展開 †
要素を%( )で囲んで埋め込みます。
request
{
_i = "pen"
"This is a %(_i)."
}
requestを実行すると、"This is a pen."が出力されます。
%( )はいわゆるeval(文字列をスクリプトコードと解釈して実行する)のような動作を行います。
単一の関数や変数だけでなく、数式を埋め込むことができます。
request
{
"1+2+3は%(1+2+3)です。"
}
requestは"1+2+3は6です。"を出力します。
文の文字列は内部にダブルクォートを含むことが出来ない点に注意してください。したがって、文字列を含む数式を埋め込むことは出来ません。以下はエラーです。
request
{
"This is a %(_i = "pen")."
}
このような場合は、埋め込みを使用せず通常の数式として結合します。
request
{
"This is a " + (_i = "pen") + "."
}
ブラケット( )による演算順序制御は範囲付き展開でも同様に働きます。~以下の例を見てください。
request
{
"遊星「%(_i = planet)」は遠い。この遊星は%(color(_i))色をしている。"
}
planet
{
"mars"
"saturn"
"pluto"
}
color
{
case _argv[0] {
when "mars"; "red"
when "saturn"; "yerrow"
when "pluto"; "blue"
others; "unknown"
}
}
最も( )が深いのはcolor(_i)の引数ですので、_i = planetの前にcolorが呼び出されてしまいます。
ブラケットを過剰付与して演算順序を調整してください。
"遊星「%((_i = planet))」は遠い。この遊星は%(color(_i))色をしている。"
これで矛盾の無い文字列が得られるようになります。
名称最長一致展開 †
( )を付与せず、単に%のみでも埋め込み展開は機能します。
request
{
o = "pen"
obj = "eraser"
object = "world"
"This is a %object."
}
obje
{
"television"
}
展開対象は%以降の文字列に一致する最も長い名前を持った変数/関数となります。上の例の場合、もっともよく一致するのは変数objectですから、これが採用されます。
結果は"This is a world."となります。
変数は刻々と作成されたり消えたりしますから、展開対象が状況によって変化することになります。
これは範囲付き展開には見られない特徴です。
request
{
val = "red"
trans
--
value = "blue"
trans
}
trans
{
"%value"
}
transは二度実行されますが、最初と二度目では"%value"の動きが変わります。すなわち、最初は変数val+"ue"、二度目は変数valueと解釈されます。
requestの出力は"redueblue"です。
%[ ]という書式で過去の展開結果を再利用できます。
request
{
"「%planet」は遠い。「%city」も遠い。もっとも%[1]になら行けなくもない。"
}
planet
{
"mars"
"saturn"
"pluto"
}
city
{
"newyork"
"moscow"
"madrid"
}
%[i]は0オリジンでi番目の展開結果を得ます。
つまり上の例では、%[0]が%planetの、%[1]が%cityの展開結果を示しています。
%[ ]は範囲付き展開では使えません。範囲付き展開で過去の結果を再利用したい場合は、それを変数へ入れてください。
名称最長一致による展開は実行する度に展開対象を検索しなおしますので、範囲付き展開と比べて動作速度が劇的に遅いです。
本当に必要な場合は別ですが、通常は範囲付き展開を利用すべきでしょう。
フロー制御 †
ifによる分岐 †
式の結果が真であれば以降に続く{ }内を処理します。
request
{
if !i {
"iは0である。"
}
}
すぐ後にelseif節を付加することができます。これはifの判定が偽であった場合のみ処理されます。
いくつでも連結できます。
また、if~elseifの最終端にはelse節を置くことができます。これは先行するifおよびelseifの判定がすべて偽であった場合に
処理されます。
request
{
if !i {
"iは0である。"
}
elseif i == 5 {
"iは5である。"
}
elseif "A" _in_ TOUPPER(i) {
"iは文字列で、aもしくはAを含んでいる。"
}
else {
"iは0でも5でもaを含む文字列でもない何物かである。"
}
}
if、elseif、elseが処理する{ }内にスクリプトが1つしか存在しない場合は、{ }を略せます。したがって上の例は
下のように書きなおすことが出来ます。
request
{
if !i
"iは0である。"
elseif i == 5
"iは5である。"
elseif "A" _in_ TOUPPER(i)
"iは文字列で、aもしくはAを含んでいる。"
else
"iは0でも5でもaを含む文字列でもない何物かである。"
}
ただしifが重なっている場合は{ }の省略は出来ません。以下はC言語では正しいですが、文では誤りです。
if i == 0
if j == 0
"iとjはともに0である。"
以下のように{ }が必要です。
if i == 0 {
if j == 0
"iとjはともに0である。"
}
C言語と同様に、ifの判定式は全体をブラケット( )で囲むことができます。
動作は囲まない場合と変わりません。
if、elseif、case、while、for、switchの判定式で使用できます。
caseによる分岐 †
caseはラベル分岐構造を実現します。
request
{
case i {
when 0 {
"iは0である。"
}
when "A"
"iは文字列Aである。"
others {
"iは0でもAでもない。"
}
}
}
caseに与えられた判定式の結果に一致するラベル値を持ったwhen節が実行されます。
others節は、すべてのラベルに合致しなかった場合に実行される部分です。othersは省略できます。省略した場合は何もしません。
ヒットさせるラベル値はカンマで列挙可能です。またマイナス符号でヒットさせる範囲を指定することもできます。
request
{
case name+(i+1) {
when "Pentium3","Pentium4"
"Pen!!!は1999年、Pen4は2000年発売発売。"
when "Pentium5"-"PentiumX" {
"まだ無い。"
}
others
"分からない。"
}
}
whenに記述するラベル値は必ず即値でなければなりません。変数や関数、演算子を含む数式は記述できません。
when、othersが処理する{ }内にスクリプトが1つしか存在しない場合は、ifと同様に{ }を省略できます。
switchによる分岐 †
{ }内の出力候補から出力は無作為に選ばれますが、switchを使用すると選択する候補をを位置で指定できます。
request
{
switch id {
"idは0である。"
"idは1である。"
{
"idは2である。"
"idはtwoである。"
}
"idは"3である。"
}
}
変数idの値によって出力される文字列が指定されます。指定は0オリジンです。
idが2の時は、"idは2である。"もしくは"idはtwoである。"が出力されます。この内包された{ }部分では、選択は無作為です。
switchが評価する値に対応する候補が{ }内に存在しない場合は空の文字列が出力されます。たとえば、上の例においてidが100だった場合は空の文字列が出力されます。
switch内に出力確定子がある場合は、各ブロックの該位置にある候補が選択されます。
request
{
switch 1 {
"かわいい"
"天才"
"サル"
--
"とは言い難い"
--
"ですね。"
"かもしれません。"
}
}
requestの出力は"天才かもしれません。"となります。
中間のブロックには指定位置に候補が無いので、出力が空の文字列となっていることに注意してください。
ループ †
while、for、foreachの3種類のループ構造があります。
while †
whileが評価する式が真である間は{ }内を繰り返し処理します。
request
{
_i = 1
_j = 0
while _i < 11 {
_j += _i
_i++
}
"1から10をすべて足すと%(_j)である。"
}
上の例はwhileの機能を簡単に説明しています。
下の例では異なる10個の文字列を発生しています。requestの出力は、1~10のうちいずれかの平方根を報告する文字列です。
request
{
_i = 1
while _i < 11 {
"%(_i)の平方根は%(SQRT(_i))である。"
_i++
}
}
for †
forはwhileと同様の先判定ループ構造ですが、初期化式、脱出判定式、ループ毎に実行する式を一箇所にまとめて書ける点が優れています。
以下は、whileで挙げた平方根を報告する例をforで書き直したものです。
request
{
for _i = 1; _i < 11; _i++ {
"%(_i)の平方根は%(SQRT(_i))である。"
}
}
_i = 1はループを始める直前に実行されます。_i < 11はループ判定式で、これが真である間はループが続きます。_i++はループの一単位が完了して先頭へ
戻る際に実行される式です。
C言語では for ( ; ; ) とすることで無限ループとできますが、文では各式を省略できません。
無限ループを作る場合は
for 1;1;1
などとしてください。ただ、whileなら
while 1
で済むため、文で無限ループを作る際は、可読性の点からも、動作速度の点からも、whileを使用すべきであると言えます。
foreach †
簡易配列、もしくは汎用配列の各要素値を順番に取り出します。
以下では簡易配列の要素値を取り出して数値へ変換し、すべての合計を計算しています。
request
{
_str = "1,3,5,7,9"
_t = 0
foreach _str; _i {
_t += TOINT(_i)
}
_t
}
foreachに続いて処理対象を記述します。上の例では簡易配列_strを指定しています。次に書かれた_iは取り出された要素値を格納する変数で、これは
必ず変数でなければなりません。
処理対象の変数のデリミタがSETDELIMによって変更されていても、foreachはそのデリミタを認識して正常に動作します。
foreachは汎用配列も処理できます。
request
{
_sent = ("I", "am", 31, "years", "old.")
_t = ""
foreach _sent; _i {
_t += (_i + " ")
}
_t
}
requestは"I am 31 years old. "を出力します。
foreachループ内において要素取り出し対象の簡易配列、汎用配列を書き換えてもかまいません。
変更は正常に反映されます。
break †
ループ中にbreakが現れると、現在実行中の最も深いループから脱出します。
request
{
_j = 0
for _i = 0; _i < 100; _i++ {
_j = _i*_i
if _j >= 100
break
}
_i - 1
}
上の例では、forは初期値0の_iが100に達するまでループを実行しようとします。しかし、ループ内には「_i を二乗した結果が 100 を越えたらループから抜ける」ようにbreakが仕込まれています。したがって、実際には _i = 10 の時点でループが終了します。
requestは_iから1を減じた価を返しています。つまりrequestは、二乗した結果が100を越えない最大の整数を求める関数です。
continue †
ループ中にcontinueが現れると、その位置からすぐにループ先頭へ戻ります。
request
{
_j = ""
for _i = 0; _i < 3; _i++
{
_j += "go "
if _i > 0
continue
_j += "ahead "
}
_j
}
_iは0、1、2と変化しますが、1、2ではcontinueが働くので、_jに"ahead"を追加する式が実行されません。
したがってrequestの出力は"go ahead go go "となります。
return †
returnが現れると、その関数の実行はそこで終わります。
関数の出力はそれまでに蓄積された候補から選ばれます。
to_rad
{
if GETTYPE(_argv[0]) == 3 {
-1
return
}
_argv[0]*2.0*3.14/360.0
}
関数to_radはdegreeをradianへ変換します。
引数に文字列が与えられた場合は、if判定でそれを発見して-1を返すようにプログラムされています。returnの時点で出力の候補は-1しか
ありませんから、これが出力されることになります。
プリプロセス †
プリプロセスは辞書ファイルを読み込んでいる段階で実行される命令です。
#define †
辞書ファイルから読み込んだ(パース前の)生の文字列に対して、直接文字列置換を実行します。
#define before after
とすると、これを記述した行より後ろにbeforeが見つかるたびに、それがafterへ置きかえられます。
#define の有効範囲は、宣言した次の行から、そのファイルの終端までです。
置換は記述順に行われますので、先に変換しておきたいものを先に書いてください。
#globaldefine †
#globaldefine before after
機能は #define ディレクティブと同じです。ただし、有効範囲が異なります。
#globaldefine を宣言すると、次の行以降の全ての範囲(その後に読み込まれる辞書ファイルも含む)で有効になります。
たとえば最初に読み込む辞書ファイルの先頭に #globaldefine を記述すると、その効果はすべての辞書ファイルに及ぶことになります。
#defineが先に処理されます。。#globaldefine は、#define 置換のあとで実行されます。
#globaldefine tea green
#define tea milk
"teacup"
置換結果は"milkcup"となります。
予約語 †
以下の単語はシステム関数名、及び制御命令名です。
これらの名前は文システムで予約されています。これらと完全に一致する名称の変数や関数をユーザー側で作成、利用することは出来ません。
システム関数 †
../システム関数で記述されている関数は全て利用できません。
今後の拡張も考えると、大文字+アンダースコアのみで構成される名前は使わないほうが良いでしょう。
制御構造キーワード †
if elseif else case when others switch while for break continue return foreach
演算子 †
以下の単語/文字は演算子です。
これらと完全に一致する名称の変数や関数をユーザー側で作成、利用することは出来ません。また、変数や関数名の一部にこれらの単語/文字を含むことも出来ません。
( ) [ ] ! ++ -- * / % + - & == != <= >= < > _in_ !_in_ && || = := += -= *= /= %= +:= -:= *:= /:= %:= ,=
謝辞 †
以下のライブラリを利用もしくは参考にさせていただきました。感謝致します。