1. トップ
  2. HSP講座
  3. >ここ

変数を返す関数

HSPの関数は、str, double, int 型の値しか返すことができません。
ここでは、マクロを駆使して「変数を返す関数」を作るテクニックを紹介します。

  1. サンプル: 値を返す
  2. サンプル: クローンを返す
  3. 定義
  4. マクロ
  5. ref_xs の解剖
  6. dict_at_ref の解剖
  7. さらなる一般化
  8. 修飾された識別子
  9. 静的変数の危険性!
  10. おわりに

サンプル: 値を返す

作り方の前に、「変数を返す関数」の使用例からみていきましょう。
「ここから」〜「ここまで」が関数の定義ですが、これは後半で説明します。

//-------- ここから ----------
#module
#define global ctype ref_xs(%1) %tref %i0 %p(ref_xs__(%1, %p)) %o0
#defcfunc ref_xs__ str value, array ref
	ref = value : return 0
#global
//-------- ここまで ----------

//使用例

	//NG
	//x = strmid("--opt", 2, 3)
	
	//OK
	x = strtrim(ref_xs("--opt"), 2, 3)
	
	mes "{" + x + "}" //→ "{opt}"
	stop

strtrim 関数は、受け取った文字列の一部を返す関数です。
厳密に言えば、これは「文字列の」ではなく「文字列の入った変数」しか受け取ることができません。
しかしこのサンプルでは、文字列の値を ref_xs 関数でくくることで、strmid に文字列の値 "--opt" を渡せています。
これは、上で定義した ref_xs 関数が「文字列の入った変数を返している」からなのです。

ref_xs は、次のサンプルも手堅い仕事を見せてくれます。


どうしてリファレンス

ところで、strmid 関数は受け取った「変数」に書き込み操作をしません。
そのため、文字列を受け取ってもいいはずです。なぜそうしないのでしょう?
実際の理由は知りませんが、少なくとも、効率面で利点があるのは確かです。[脚注]

文字列を値として受け取るとき、HSPはその文字列の「コピー」(写し)を作ります。
文字列をコピーする操作には、その文字列が長ければ長いほど、時間がかかるものです。
一方で、変数を受け取るとき、HSPはその変数をコピーするのではなく、「どの変数を渡したか」だけを伝えます。
この操作は、その変数が持っているデータにかかわらず一瞬です。
そのため、もし私が strmid 関数みたいなのを作るとしても、全体のコピーを回避するために変数を受け取るようにするかもしれません。

※実際には値を取るバージョンと変数を取るバージョンの2パターンをいちいち作るという、無駄の多い生涯を送って来ました。


サンプル: クローンを返す

もう1つサンプルです。
この記事のタイトルの通り、先程の ref_xs 関数は「変数を返す関数」です。
「どの変数を返すのか?」というと、「新しい変数」を作って、それを返却していました。

今回のサンプルでは、「既にある変数を返す」関数を作りましょう。
技術的な理由で、それは難しいので、「既にある変数のクローンを返す」関数を作ります。
※「クローン変数」については dup 命令のヘルプを参照。

ちょっと長いですが、「#module」〜「#global」の間は、命令の意味だけ把握できれば十分です。
あるいは、最後のほうのサンプルだけ読んでも意味が分かるようになっている……はずです。

// ref_xs 再掲
#module
#define global ctype ref_xs(%1) %tref %i0 %p(ref_xs__(%1, %p)) %o0
#defcfunc ref_xs__ str value, array ref
	ref = value : return 0
#global

#define global ctype ref_expr_tmpl_1(%1, %2) %tref %i0 %p(%1(%2, %p)) %o0

// 簡単な辞書モジュール
#module static_dictionary

// 「文字列 → vtype 型」の辞書として初期化する
#deffunc dict_init int vtype
	sdim    stt_keys          // キーの配列
	dimtype stt_vals, vtype   // stt_vals(i) が、stt_keys(i) に対応する値を持つ。
	stt_len = 1               // 登録されているキーの数 + 1
	return
	
// 辞書に「key → value」という対応を登録する
// すでに「key → なにか」があったら、上書きする
#deffunc dict_add str key, var value,  \
	local i
	
	i = dict_find_index(key)
	if ( i == 0 ) { i = stt_len : stt_len ++ }
	stt_keys(i) = key
	stt_vals(i) = value
	return
	
// 辞書から key に対応する添字を探す。
// なかったら 0 を返す。
// (実装が雑)
#defcfunc dict_find_index str key,  \
	local i
	
	for i, 1, stt_len
		if ( stt_keys(i) == key ) { return i }
	next
	return 0
	
// 辞書の key に対応する要素の値を返す。
#defcfunc dict_at str key
	return stt_vals(dict_find_index(key))
	
// 辞書の key に対応する要素へのクローンを返す。
#define global ctype dict_at_ref(%1) \
	ref_expr_tmpl_1(dict_at_ref__,%1)

#defcfunc dict_at_ref__ str key, array ref
	dup ref, stt_vals(dict_find_index(key))
	return 0
	
#global

	// 文字列から値への辞書を作る。
	// 値の型は、今回は文字列(str)型にする。
	// (int, double でもいいようになっている。)
	dict_init vartype("str")
	
	
	// 「朝のあいさつ」の内容を「おはよう」とする。
	// ※dict_add は値ではなく変数を受け取るので、ref_xs 関数を介する。
	dict_add "朝のあいさつ", ref_xs("おはよう")
	
	// 「朝のあいさつ」の内容を表示する。
	mes dict_at("朝のあいさつ")
	//→ "おはよう"
	
	
	// 「4文字の単語」の内容を「いっぱい」にする。
	dict_add "4文字の単語", ref_xs("いっぱい")
	
	// 「4文字の単語」の内容の値を、strrep で置換する。
	strrep dict_at_ref("4文字の単語"), "い", "お"
	
	mes "「いっぱい」の「い」を「お」に変えて言ってみて?"
	mes dict_at("4文字の単語")
	//→ "おっぱお"
	stop

男子小中学生がよくやるやつ。◇猥褻が一切ない◇

さて、「朝のあいさつ」のほうは、モジュールの簡単な使用例になっています。
この記事において重要なのは、後半、「いっぱい」のほうです。

辞書のデータは変数 stt_vals が記録していますが、これはモジュールの内側にあります。これに触るには、辞書モジュールの命令や関数を通さなければいけません。[脚注]
そのため、それを「変数を受け取る命令や関数」に渡すには、「そういう命令を作る」か「値のコピーを変数に代入する」か「クローンを作る」かのどれかです。

「そういう命令を作る」とは、この例でいえば「モジュールのなかの変数に strrep 命令を使う」命令を作る、ということです。
しかしこれは現実的ではありません。strrep 専用の命令を用意するなら、pokememcpystrmid のための命令も用意したいところですが、いったい何個の命令を作ればいいのでしょうか?[脚注]

「値のコピーを変数に代入」「クローンを作成」の場合はどちらにせよ、命令文を1回使うことになり、スクリプトが冗長になります。

	// 一旦、値を変数 flw にコピー
	flw = dict_at("4文字の単語")
	
	strrep flw, "い", "お"
	
	// 「4文字の単語」の内容を上書き
	dict_add "4文字の単語", flw
	// flw を、「4文字の単語」に対応する変数のクローンにする。
	// (そういう dict_dup 命令があるということにする。作るのは実際簡単。)
	dict_dup flw, "4文字の単語"
	strrep flw, "い", "お"

dict_dup 命令は便利っぽいですが、次のように書くこともできます。

	dup flw, dict_at_ref("4文字の単語")

ところで、この辞書モジュールは「str→str」だけでなく「str→double」や「str→int」としても使えます。
(定義をうまくすれば「double→str」なんかも作れますが、さすがに使わないと思うので、キーは文字列に固定しました。)

//----
// ref_xd
#module
#define global ctype ref_xd(%1) %tref %i0 %p(ref_xd__(%1, %p)) %o0
#defcfunc ref_xd__ double value, array ref
	ref = value : return 0
#global
//----

	// 今回は「文字列→小数」として使う
	dict_init vartype("double")
	
	// 円周率の値を登録
	dict_add "円周率", ref_xd(M_PI)
	
	mes dict_at("円周率")
	//→ 3.14159
	stop

ref_xd は ref_xs 関数の double 型バージョンです。

1つのスクリプトで2つ以上の辞書を作りたいって?
モジュールクラスの世界へようこそ!


定義

ここから、仕組みを説明していきます。
とはいえ、実装の短さから察せる通り、それほど難しいことはしていません。

マクロ

マクロの意味を知っている読者は goto してください。

マクロとは、スクリプトの断片を命令や関数のように書く機能です。
詳しくは #define 命令のF1ヘルプや解説記事などを読めば分かりますが、今回使用する機能だけ抜粋して、簡単に説明しておきます。

// 関数マクロ quote を定義
// 「ctype」を使うと、関数っぽく quote(...) と書くことになる。
#define ctype quote(%1) ("「" + (%1) + "」")

	// マクロを使った式
	mes quote("こんにちは")

	// ↑の式は、自動的に↓の式に書き換えられる。

	mes ("「" + ("こんにちは") + "」")

#define などのプリプロセッサ命令のある行は、行の最後に円記号「\」を書くことで、改行できます。
見やすくするために、今回は改行を多用することにします。

// ↑と同じマクロ
#define ctype quote(%1) (    \
	"「" + (%1) + "」"        \
	)

HSPのマクロは、単純な置き換え以上の仕事ができます。その1つの機能が「一意な識別子の生成」、すなわち、ほかの変数やラベルの名前と重複しない名前を自動的に作ることです。

#define forever \
	%tforever /* ← %i などを書くのに必要。意味を知る必要はない。 */ \
	%i0       /* ←一意な識別子を作る */ \
	*%p       /* ←作った識別子 %p に * を付けてラベルにする */

#define forever_loop \
	%tforever  /* ← forever のときと同じことを書く */      \
	goto *%p   /* ← forever が配置したラベルにジャンプ */  \
	%o0        /* ←識別子を使い終わったらこれを書く必要がある */

// 使用例

	forever
		mes "hello!"
		await 1000
	forever_loop

//↑のスクリプトは、自動的に↓のように書き換えられる。

*__forever_0       // 作った識別子の名前を仮に __forever_0 とする。
		mes "hello!"
		await 1000
	goto *__forever_0


ref_xs の解剖

単純な「変数を返す関数」の例として、ref_xs の定義を詳しくみていきましょう。

//ref_xs の定義はここから
#define global ctype ref_xs(%1) \
	%tref %i0 \
	%p(ref_xs__(%1, %p)) /* (☆) */ \
	%o0
//ここまで

#module

#defcfunc ref_xs__ str value, array ref
	ref = value
	return 0

#global

ref_xs の定義を4行に分けて書きました。
このうち、最初の2行と、最後の1行は、「マクロ」の節で forever マクロが既にやったことです。
重要なのは (☆) の行だけであり、実際、 ref_xs() マクロはこの行に書かれている内容に置き換わります。

(☆) の行において、「%p」は「ある一意な識別子」、ここでは「ほかの場所で使われることがない変数の名前」を表しています。
「%p」の位置は、スクリプトに ref_xs と書くごとに別の変数の名前になるのです。

さて、変数「%p」の後に丸括弧があるということは、これは配列要素ですね。配列変数「%p」の、「ref_xs__(%1, %p)」番目の要素です。
この ref_xs__ 関数がキモです。先回りしてその定義を読んでみると、これは引数「%1, %p」とは無関係に 0 を返しますね。
一旦まとめると、マクロの式「ref_xs(...)」は、「%p(0)」という式を意味しているわけです。%p が int や str などの普通の型なら「%p(0)」と「%p」は同じですから、ref_xs は %p を返すことになります。

ref_xs のなかで、関数 ref_xs__ が呼び出されます。引数に注目すると、次のような関係になっています。
「%1」=「value」=「ref_xs に渡された文字列」
「%p」=「ref」=「ref_xs が表している変数」
ref_xs__ の定義は、「%p に与えられた文字列を代入する」ものです。(そして 0 を返す。)

結局、ref_xs(...) という式は1つの変数であり、配列の添字のなかで文字列を代入した、ということです。

ちなみに、ref_xs 関数を使うまで、変数 %p は 0 になっています。つまり int 型です。
そして、HSPは式「%p(...)」をみると、この変数の型を int 型だと認識します。
そのため、「ref_xs("x")」という式のは int 型です。代入文とかで使ってはいけません。

#module
#define global ctype ref_xs(%1) %tref %i0 %p(ref_xs__(%1, %p)) %o0
#defcfunc ref_xs__ str value, array ref
	ref = value : return 0
#global

	//※こういう式を書いてはいけない!
	x = ref_xs("hello")
	
	mes vartype(x)  //→ 4 (整数)
	mes x           //→ よく分からない整数値

dict_at_ref の解剖

2つ目のサンプルで使用した dict_at_ref 関数の定義もみておきましょう。
これは2段階に分けられます。

まず ref_expr_tmpl_1 マクロから。

// 関数 %1 に、変数と式 %2 を渡す
#define global ctype ref_expr_tmpl_1(%1, %2) \
	%tref %i0 \
	%p(  %1(%2, %p)  ) /* (☆) */ \
	%o0

ref_xs にすごく似ている……というか ref_xs の「ref_xs__」と書かれた部分を %1 にしたものですね。
これを使うと、ref_xs を短く定義できます。

// ref_expr_tmpl_1 を使って ref_xs を定義

#define global ctype ref_xs(%1) \
	ref_expr_tmpl_1( ref_xs__, %1 )

#module
#defcfunc ref_xs__ str value, array ref
	ref = value
	return 0
#global

気持ち的には、ref_xs__ 関数を定義して、ref_xs はちょこっと書いただけという感じになりました。
dict_at_ref 関数も、そんな感じです。

// 辞書の key に対応する要素へのクローンを返す。
#define global ctype dict_at_ref(%1) \
	ref_expr_tmpl_1( dict_at_ref__, %1 )

#defcfunc dict_at_ref__ str key, array ref
	dup ref, /*(★)*/(  stt_vals(dict_find_index(key))  )
	return 0

dict_at_ref 関数は、(★) の式(dup 命令の第2引数)へのクローンを返しています。

(★)の式は、指定されたキー key に対応する値を持つ変数を表しています。そのこと自体は、辞書モジュールの実装を詳しく読めば分かりますが、ここでは重要ではありません。


さらなる一般化

ref_xs と ref_xd, ref_xi は3兄弟のようなものですが、彼らだけでは終わりません。
配列にも拡張することは可能です。

#define global _empty//
#define global ctype ref_expr_tmpl_2(%1, %2 = _empty, %3 = _empty) %tref \
	%i0 %p( %1(%2, %p, %3) ) %o0

#module
#define global ctype ref_xia2(%1, %2) \
	ref_expr_tmpl_2(ref_xia2__, %1, %2)
	
#defcfunc ref_xia2__ int v0, array ref, int v1
	ref = v0, v1
	return 0
#global

// 使用例

#module
#deffunc myboxf array pt1, array pt2
	boxf pt1(0), pt1(1), pt2(0), pt2(1)
	return
#global

	color ,127
	// 点(50, 50) 〜 中央までの矩形を塗りつぶす
	myboxf ref_xia2(50, 50), ref_xia2(ginfo_winx/2, ginfo_winy/2)
	stop

この ref_xia2 関数は、2つの整数値をとって、長さ2のint配列を返します。
もちろん受け取るには array 引数を使う必要があります。

ほかにもさまざまなパラメータを与えることで、別の使いかたができるかもしれません。


修飾された識別子

ref_xs などの関数を使うと分かりますが、変数が大量に発生し、デバッグ・ウィンドウの変数リストを汚染します。
これを防ぐために、生成される識別子をモジュールの中に入れて、蓋をしましょう。

#module
#define global ctype ref_xs(%1) \
	%tref %i0 \
	%p @__ref(  ref_xs__(%1, %p @__ref)  )\
	%o0
	
#defcfunc ref_xs__ str value, array ref
	ref = value
	return 0
#global

	mes strtrim(ref_xs(" hello "))
	// デバッグ・ウィンドウで変数の名前を見てみよう。
	// ref_xs が作る一時変数に、@__ref が付いている。

注意点は、「%p」と「@__ref」の間にスペースを入れなければならないこと。このスペースはどうやら %p が削ってくれるようです。


このセクションは広告目的であり、怪しくは無い。安心です。

デバッグ・ウィンドウを筆者が改造したバージョン、「knowbug」では、変数をモジュールごとに分離して見ることが可能です。さらに、一部のモジュールに属する変数を非表示にできるオプションもついています。
変数リストが ref_xs に汚されることはありません。


静的変数の危険性!

ところで、上に書いた ref_expr_tmpl_1 (および ref_xs) の定義は、少々危ないところがあります。
自動的に作られる変数 (一意な識別子) というのは、あくまで「ref_xs」と書かれた場所ごとに一意という意味です。
そのため、1つの ref_xs 式を2回実行すると、その変数は上書きされるのです。
変数にデータを入れてから使うまでの間、あるいは使っている最中に「上書き」が起こると、大変なことになります。

ref_xs のように返値の変数の型が固定されているときは、次のように配列を使うことが可能です。

#module
#define global ref_xs_r(%1) \
	stt_ref_xs_r@__ref(ref_xs_r__(%1, stt_ref_xs_r@__ref))
	
#define stt_len stt_ref_xs_r_len@__ref

#defcfunc ref_xs_r__ str value, array ref
	ref(stt_len) = value
	stt_len ++
	return stt_len - 1
	
#global
	sdim stt_ref_xs_r@__ref, 64 // 配列を文字列型に初期化
	
	mes ref_xs_r("hello")  //ここで (0) を使う
	mes ref_xs_r("world")  //ここで (1) を使う

ref_xs_r は ref_xs の改良版です。
特殊展開マクロでいちいち「一意な変数」を作るのはやめました。ref_xs_r は常に同じ配列変数 stt_ref_xs_r@__ref の要素を返すのですが、呼び出すたびに次の要素を使うことになっています (次の要素の番号が stt_len)。これにより、ref_xs にあった上書きの問題が解消されました。[脚注]

使用する配列変数を1つに決めた利点として、その配列 stt_ref_xs_r@__ref事前に初期化しておけるようにもなりました。
つまり、初めて ref_xs_r(...) を呼んだときにもHSPはこれが文字列型であることを知っているので、これの値を正しく読み取れるのです。実際にサンプルでは、ref_xs_r() の値を文字列データとして使うことができています。

実は dict_at_ref も、今の状態なら同じ方法で救うことができます。dict_at_ref 関数が返す型は、dict_init の時点で決定するからです。
しかし現実的には、辞書を複数個作るための機能を盛りこまなければ不便でしょう。そうすると、dict_at_ref がどの型の変数を返すのかが分からないので、配列変数を返すことができません。
「str→str」の辞書しか使えないことにする、というのも現実的な対処ですが、モジュールの汎用性を損ねることになります。


おわりに

このアイディアはたぶん2008年ごろに発見して、dict_at_ref のようなクローンを返す形で、いろいろなところで使っていましたが (abdata, var_assoc など)、ref_xs のような関数が極めてクールだと気づくまでに2015年になってしまいました。
調べてみたら、さすがに見つけてる人がほかにもいるかも?