1. トップ
  2. HSP講座
  3. プリプロセッサ命令編
  4. >ここ [
    1. ←前
    2. |
    3. 次→
    ]

モジュール

今回は非常に便利なモジュール機能を使ってみましょう。

  1. 独立名前空間
  2. メイン・コンテンツ
  3. モジュラの冒険 #2 〜孤高の変数たち〜
  4. モジュラの冒険 #3 〜孤高のいろんなもの〜
  5. モジュラの冒険 最終話 〜辺境で彷徨うパラム達〜
  6. モジュラの冒険 リターンズ
  7. おわりに

独立名前空間

HSPのモジュールを一言でいうと、「独立した空間」または「外と切り離された空間」になります。

	/* ここはグローバル空間です。*/

#module mod_name

	/* ここがモジュール空間です。*/

#global

	/* ここもグローバル空間です。*/

モジュールの開始は、プリプロセッサ命令 #module を使い、
モジュールの終了は、プリプロセッサ命令 #global を使います。

コメントにも書きましたが、モジュール空間(内)でない所を、それに対してグローバル空間(外)と呼びます。


メイン・コンテンツ

モジュールの中には、当然ながらHSPスクリプトを書くことができます。
しかし、モジュールの中は普通には実行できません!

	mes "モジュール空間::前"

#module mod_name

	mes "モジュール空間::中"

#global

	mes "モジュール空間::後"
	stop

スキップされてしまいましたねぇ。

中に突撃するには、方法は2つあります。1つはラベルでジャンプする方法ですが、2つめは……
……おそらく、ここには「ユーザ定義命令」のページから来ていると思いますが、そうでない人は、順番的に、次のページを参照してください。
+参照:ユーザ定義命令

……。さて、読んだでしょうか。わかりにくかったらすいません。
上記のページでは、命令の定義部分を ラベル を用いて飛ばしていましたが、もっと適任なモノがありますね、ここに。
本当は、#module を使うんです。

	mes "モジュール空間::前"

#module mod_name

#deffunc Message
	mes "モジュール空間::中"
	return

#global

	Message
	mes "モジュール空間::後"
	stop

ちゃんと、中に入ることができましたね!
これは、ユーザ定義関数でも同様です。
モジュールの使い方 #1 は、「ユーザ定義命令を中に入れるため」です。


モジュラの冒険 #2 〜孤高の変数たち〜

↑で、「ユーザ定義命令はモジュールの中で定義すべし」的なことを言いましたが、なぜでしょうか。
ラベルで飛ばせばいいモノを、と思うでしょう。……いや、思ってください、話が繋がらないので (爆)。

	goto *main

;#module sample_mod

#defcfunc StrMul str sSrc, int times
	result = ""			// ここで変数を使っている

	if ( times <= 0 ) { return }	// 0回反復の場合は完了、負数回の反復は失敗

	repeat times
		result += sSrc
	loop
	return result

;#global

*main
	result = "RESULT変数の中身。\n"	// 外でも result を使っている

	mes StrMul(result, 5)		// result の反復を任せる
	mes result			// result * 5 になってしまった!
	stop

さて、ラベルで定義をスキップするタイプです。コメントを外した場合と、動作を比べてみてください。

どうでしょうか。ラベルを使ったタイプだと、定義はグローバル空間にあります。
それに対して、モジュールを使うタイプだと、定義はモジュール空間内に収まります。
モジュール空間内にある変数は、外部の変数と独立しているのです!
そのため、モジュール内の変数 result を弄くっても、外にある変数 result には影響を及ぼしません。

モジュール内の変数は、外界と切り離され、独立しています。
こうすることによって、他のユーザが作ったスクリプトを、自分のスクリプトに簡単に組み込むことが可能になります。
もし変数が独立してくれないとすると、モジュール内で使われている変数を調べ、外の変数と同じ名前のものがないかを入念にチェックし、修整し、…………あぁ”、めんどくさい!! ってなりますよねぇ。
しかし、モジュールが外と決別しているため、そんなことに一切気を遣わず #include 命令でスクリプトを連結することができます。

モジュールの使い道 #2 は、「変数をグローバル空間から独立させるため」です。
ちなみに、このような変数をモジュール内変数とか静的ローカル変数とかなんとか言うといいんじゃないでしょうか (例によって正式名称ではないですが)。
注意として、間違ってもこれをモジュール変数と呼んではいけません。


モジュラの冒険 #3 〜孤高のいろんなもの〜

さて、流れ的に読まれたかも知れませんが、独立するのは変数だけではありません。
マクロもしっかり独立してくれます。

#module mod_hello_world

#define STR_HELLO_WORLD "- HELLO WORLD! -"

// モジュール内の STR_HELLO_WORLD の文字列を返す
#defcfunc GetHelloWorld
	return STR_HELLO_WORLD

#global

// 外でも同じものを定義する
#define STR_HELLO_WORLD "Hello, world!"

	mes "外:"+ STR_HELLO_WORLD
	mes "中:"+ GetHelloWorld()
	stop

ちゃんと独立できました。マクロを一部で独立されられるのは、ちょっと珍しいですね。
ただし、global で定義していた場合は別です。これは、敢えて内外両方で使えるようにするものなので。


モジュラの冒険 最終話 〜辺境で彷徨うパラム達〜

モジュラの冒険……いったいどんな話なんだろ。

#module にはモジュールの内と外で「名前被り」が起こらないようにする機能がありましたが、#module の中で「名前被り」が起こらないようにするための機能があります。
次の例を見てください。

#module mod_string_utilities

//------------------------------------------------
// 文字列反復の関数
// 
// @prm sSrc  : 対象の文字列
// @prm times : 反復回数 ( 0 以下の時、空文字列 )
// @return    : 反復した文字列
//------------------------------------------------
#defcfunc StrMul str sSrc, int times,  local result
	result = ""			// ここで変数を使っている

	if ( times <= 0 ) { return }	// 0回反復の場合は完了、負数回の反復は失敗

	repeat times
		result += sSrc
	loop
	return result

/*------------------------------------------------
 * 文字列を削除する
 * p1 から、指定バイトの文字列を削除します。
 * @prm p1 = var	: 文字列型変数
 * @prm p2 = int(0)	: オフセット
 * @prm p3 = int	: 削除する長さ
 * @return = int	: 文字列の長さ
 *-----------------------------------------------*/
#deffunc StrDelete var result, int offset, int size,  local iSpace
	iSpace = strlen(sBuf) - (offset + size)		// 後ろに出来る空白の大きさ
	result = strmid(result, 0, offset) + strmid(result, offset + size, iSpace)
	return

#global

	mes StrMul("Hello! ", 3)

※重要ではないが、コメントの書き方を二通り載せた。他にもさまざまな流儀がある。

この2つの命令・関数はどちらも result という名前を使っていますが、「StrMul 関数の中の result」と「StrDelete 命令の中の result」は互いに別のものです。
後者が var 引数エイリアス、つまり引数に与えられた変数を表していることは知っていると思いますが、では前者は何でしょうか?
これが「ローカル変数」——実を言うと、ほとんど普通の変数です。

ローカル変数を宣言するには、パラメータの種類として local を選びます。
これは実引数を受け取りません。つまり、命令を呼び出す側では local 引数は無視します。

#defcfunc StrMul str sSrc, int times,  local result

こうしておくことで、この変数はこの命令の中でしか使えなくなり、他の命令でうっかり同じ名前を使っても大丈夫です。
試しに最初の例で、StrMul の定義から「local result」を削除してみてください。よく分からないエラー(「error 26 : パラメーター引数名は使用されています」[脚注])が出ます。

ローカル変数の一番の特徴は、命令の中でのみ有効ということです。[脚注]
命令が呼び出されると、宣言されたローカル変数が作成されて、命令が終了すると、呼び出されたときに作られたローカル変数がすべて破棄されます。[脚注]
この性質は「再帰呼び出し」(recursive call)に利用できます。
再帰呼び出しとは、命令や関数を、それ自身の定義の中から呼び出すことです。まずはローカル変数を使わない、単純な再帰呼び出しの例から。

#module

// 再帰呼び出しのよくある例:階乗
#defcfunc fact int n
	assert n >= 0
	if ( n == 0 ) {
		return 1
	} else {
		return n * fact(n - 1)	// ← fact自身を呼んでいる
	}

#global

// 使用例
	mes "fact(4) = " + fact(4)	//= 4 * 3 * 2 * 1 * 1

fact は「階乗」(factorial)という関数を表しています。計算過程がかなり興味深いですね。

 4! = fact(4)
     = 4 * fact(3)
     = 4 * (3 * fact(2))
     = 4 * (3 * (2 * fact(1)))
     = 4 * (3 * (2 * (1 * fact(0))))
     = 4 * (3 * (2 * (1 * 1)))             (fact(0) = 1)
     = 24

再帰呼び出しが行われる命令の中の変数は、ローカル変数にしておかないとまずいことがたびたびあります。
どの変数をローカル変数にするべきか、をいちいち考えるのは大変なので、全部ローカル変数にしておくのが無難でしょう。


#module

#deffunc rec int n //, local a
	if ( n <= 0 ) { return }
	a = n
	rec n - 1 //この中で a の値が変わる!
	mes a
	return

#global

	rec 2

ちなみに、再帰呼び出しの回数には限界があり、超えてしまうと「スタック領域のオーバーフローです」というエラーが出ます。
HSPで再帰できる回数はあまり多くないので、注意が必要です。
というより、再帰をやめて反復(ループ)を使うという手もあります。

#module
//フィボナッチ(Fibonacci)数列、単純な再帰による定義
#defcfunc fib int n
	assert n >= 0
	if ( n == 0 ) { return 0 } //(*1)
	if ( n == 1 ) { return 1 } //(*2)
	return fib(n - 1) + fib(n - 2) //(*3)
#global
#module
//フィボナッチ(Fibonacci)数列、動的計画法(dynamic programming)による定義
#defcfunc fib int n
	assert n >= 0
	
	fib_dp(0) = 0 //(*1)
	fib_dp(1) = 1 //(*2)
	if ( n >= 2 ) {
		repeat (n - 2) + 1, 2
			fib_dp(cnt) = fib_dp(cnt - 1) + fib_dp(cnt - 2) //(*3)
		loop
	}
	return fib_dp(n)
#global

	// 使用例
	repeat 15
		mes strf("fib(%d) = %d", cnt, fib(cnt))
	loop

fib の数学的な定義は (*1), (*2), (*3) です。
(*3) の部分では再帰呼び出しを使うほうが自然ですが、上のスクリプトでは配列 fib_dp の値を順繰りに計算していくことで、再帰ではなくしています。[脚注]

発展的かつ実用的な例として、マージソートをループで書いたもの(他所様)もあります。


モジュラの冒険 リターンズ

さて、これでユーザ定義命令も完結です。
……が、一つ、非常に非常に重要なテクニックがあります。
引数を省略したときの値、つまり省略値を定義する方法です。

#module mod_sample

#define global TextOut(%1="") _TextOut %1
#deffunc _TextOut str sMessage
	mes sMessage
	return

#global

	TextOut			// 省略!
	TextOut "Hello, world!"
	stop

マクロ・パラメータの省略値補完の機能を使います。
まず、命令名を _TextOut のように、適当な名前にしておきます。
実際の命令は、#define で引数付きマクロとして定義しておきます。

また、マクロを用いて次のようなこともできます。

#module mod_sample

#define global TextOut(%1="") _TextOut ""+ (%1)
#deffunc _TextOut str sMessage
	mes sMessage
	return

#global

	TextOut	M_PI // 円周率π
	TextOut 0xFF00FF00
	stop

HSPの原則で、
「式の型は、一番左の値の型と一致する」
というのがあります。
TextOut 命令の引数に与えられた値を ""+ を使って文字列型にすることで、型不一致エラーになることはありません。
これによって擬似的に、複数の型に対応した命令っぽくできます。

#module mod_sample

#define global ctype EnvironQuote(%1="") _EnvironQuote("" + (%1))
#defcfunc _EnvironQuote str sSrc
	return "\""+ sSrc +"\""

#global

	mes EnvironQuote("Hello, world!")
	stop

関数形式のときに、どぎまぎしてしまう人が多そうなので。


おわりに

さて、これでユーザ定義命令も(本当に)完結です。
次回以降もモジュールは多用します。慣れていきましょう。