ソラマメブログ

2007年06月06日

関数(lsl基礎知識 第五回)

今回は「関数」についてのお話です。
基礎知識の今までの記事の中でも何度か唐突に「関数」という言葉が出てきたかと思いますが、一体それは何なんだというところから話を始めたいと思います。
関数って何だ

一般的に関数と言うと、
y = ax + b

こんなのを思い出すかと思います。

中学校あたりの数学で出てくる「一次関数」というヤツですね。
これ、要するにxの値が変わると、それに関連してyの値も変わることから、「関数」と言われるんですね。

この数学用語を英語で言うと「function(ファンクション)」です。

ファンクションの意味を『新英和中辞典 第6版 (研究社)』で調べてみると、

1 機能,働き,作用,目的
2-a 職務,職能,職分,役目
  b 【文法】 機能
3-a 儀式,行事; 祭典,祝典
  b 《口語》 (規模の大きい)社交的会合,宴会
4a 他のものに関連して変化するもの 《性質・事実など》, 相関関係
  b 【数】 関数

こんなんが出てくるわけです。

最近のプログラムで“ファンクション”と言うと、「機能」の意味と「関数」の意味を併せ持っています。
「機能」というのは、「何かまとまった処理を行うもの」のことです。
一方「関数」のほうは、「パラメータを与えると(あるいはパラメータが無い場合もあるが)答えを返すもの」のことを言います。

この「機能」と「関数」のことを総称して本来は「サブルーチン」と言うのですが、最近は「サブルーチン」=「ファンクション」=「関数」の意味で使われることが多いようです。

ということは、「何かまとまった処理を行う」だけで、何もパラメータがなかったり、答えも返さない場合でも「関数」と呼ばれるわけで、少々ピンと来ない用語になってしまっています。
まあ、ファンクションを「宴会」と訳されるよりはマシだと思うことにしましょう。

前置きが長くなりましたが、lslで言うところの関数というのは
「何かまとまった処理を行うもの」
「パラメータを与えると答えを返すもの」
のことです。

例えばlslでは、
・チャットで何かしゃべらせる
・オブジェクトの色を変える
・テクスチャを変更する
・音を鳴らす
・アバターにアニメーションをさせる
これらは全て関数です。
どれもこれもSLにおける「機能」を実現するものです。

一方、以下のようなものは、
・小数値を四捨五入する
・文字列の一部を取り出す
・オブジェクトの指定面の透明度を得る
・指定したオブジェクトのオーナーを調べる
これらは、与えたパラメータに応じて結果が変わりますので、“関数”の名に相応しいのかもしれません。

「他のものに関連して変化する=パラメータを与えると答えを返す」ということは、関数には「パラメータ」と「答え」があります。
「パラメータ」のことをプログラミング用語では「引数(ひきすう)」と言います。
「答え」は「戻り値」「返り値」と言います。

たとえば・・・

新入社員のA君は、掃除の達人です。
課長が「おい、A君」と声をかけるだけでサッと席を立ち、手早くオフィスを綺麗にしてくれます。
一通り掃除を済ますと黙って席に戻ってしまうので、どこをどう掃除したのやらわからないのですが、オフィスを見ればなんだか綺麗になっているので、A君が掃除をしてくれたことがわかります。
しかしながら、「おい、A君」と言っただけで掃除を始めてしまうため、
「今日はロッカーの辺りを頼むよ」
とか
「掃除じゃなくて書類の整理をしてくれ」
などの課長の細かい指示には一切耳を傾けません。

これは「引数」も「戻り値」もない、「何かまとまった処理をする」だけの関数の例です。
引数が無いため、課長の細かい指示は受け付けません。
戻り値も無いので、掃除が終わったあとの報告もありません。
ですが一応掃除はきっちりとこなしてくれるのです。

一方、入社2年目のBさんは、お茶汲みの達人です。
課長が「ちょっと、Bさん」と声をかけると、あっという間に席を立ち、湯飲みにお茶を入れて持ってきます。
ときどきうっかりして、お茶の葉を入れずにただのお湯を持ってきたりしますが、課長のところに湯飲みを持ってくるのを忘れることは決してありません。
たまには課長も、
「今日はコーヒーにしてくれないか」
とか
「大至急この書類をコピーしてくれ」
など、別の指示を出したいことがあるのですが、「ちょっと、Bさん」と言っただけで疾風のようにお茶汲みに行ってしまうため、指示が出せません。

これは「引数」が無く、「戻り値」のある関数のパターンです。
指示は一切出せませんが、必ず湯飲みは届きます。
湯飲みの中身がお茶である保証は無く、なんらかの要因で「戻り値」は変化します。
ですが、何があろうとも必ず湯飲みだけは持ってきてくれるBさんなのです。

さて、入社4年目のC君はどうでしょう。
彼はさすがにA君やBさんよりもしっかりしています。
課長が「なぁ、Cくん」と声をかけ、「会議の資料をまとめてくれ」と頼むと、
「資料は何部必要でしょうか?」
必ず資料の部数を確認してきます。
「10部頼むよ」
そう課長が言うや否や、怪鳥のように飛び上がったC君は、瞬く間に10部の資料をまとめ上げます。
「今日は400部は必要だな」
と言われたときも、何の不満も言わずに仕事をこなします。
ですがC君、最後の詰めがまだまだです。
出来上がった資料を自分のデスクに置いたまま、課長には何の報告もしないのです。
さらに困ったことに、課長がときどき、
「C君、今日は一杯いくか」
とか
「取引先に電話してくれ、C君」
などといつもと違った指示を出したときにも、
「何部でしょうか?」
真面目な顔をしてそう問い返してくるのです。

これは「引数」があって「戻り値」が無い例です。
課長は資料の部数をC君に指示することができます。
ですが、C君からの報告はありません。
また、指示することができるのはどんな場合でも部数だけです。
C君は部数以外の指示を受け付けることができないのです。

最後に、秘書のDさんです。
彼女はあらゆるスケジュールを記憶しています。
課長が、
「Dさん、明日の午前中の社長の予定はどうなっていたかな?」
そう問いかけた瞬間、Dさんの目が針のようにギラリと光り、
「明日は大阪支社に出張の予定です」
瞬時に社長の予定を教えてくれます。
「じゃあ専務の明日の夜の予定は?」
と聞けば、寸分の迷いもなく、
「愛人と名古屋のホテルに泊まる予定です」
極めて明瞭な答えが返ってくるのです。
しかしながら、Dさん、スケジュール以外には興味がありません。
「お昼を一緒にどう?」
とか
「今度デートしないか」
などと声をかけても、
「私は秘書です。どなたのいつの予定をお知りになりたいのです?」
毅然とした態度でそう言われてしまいます。

Dさんは「引数」も「戻り値」もある関数の例です。
引数としては、「誰」の「いつ」の予定なのか、この二つを指示することができます。
そして戻り値は「予定」です。
関数の「引数」はあらかじめ決まっていますので、「誰」「いつ」以外のことは指示できません。
また、戻り値の内容も「予定」のみに限られます。

まとめておきます。

関数処理内容引数戻り値
Aくん掃除なしなし
Bさんお茶汲みなし湯飲み
Cくん資料作成部数なし
Dさんスケジュール管理誰、いつ予定


引数や戻り値があったりなかったりしますが、全て何らかの機能を実現しています。
この喩えから言えることは、何はともあれこんな会社には勤めたくないなという点です。

関数の使い方

下らないことに文字数を費やしました・・・。
具体的な話に入りましょう。

関数は以下のような構文になっています。

戻り値の型 関数名(引数1, 引数2...)

例えば、UUIDから名前を調べる関数llKey2Nameというのがありますが、この関数はUUID(key型の値)を引数に持ち、戻り値は文字列型の値です。

string llKey2Name(key id)

引数が二つ以上になる場合は、引数の間を「,」で区切ります。
例えば文字列の一部を取り出す関数llGetSubStringでは、「どの文字列」の「何文字目」から「何文字目」を取り出すのかを引数で指定します。
戻り値は文字列型です。

string llGetSubString(string src, integer start, integer end)

関数の戻り値は変数に代入できますので、具体的には以下のように使います。

string sub = llGetSubString("abcdefg", 0, 3);

文字列型の変数subに、llGetSubString関数の戻り値を代入しています。
llGetSubString関数は「文字列"abcdefg"」の「0番目」から「3番目」を抜き出します。
0番目という表現は奇妙に見えるかもしれませんが、多くのプログラミングでは先頭は0で示されます。
これをゼロ起算と言います。
「文字列"abcdefg"」の「0番目」から「3番目」というのはつまり、「"abcd"」のことになります。
従って、変数subには"abcd"が代入されるわけです。

これって要するに戻り値のある関数を計算式の一部として使えるということです。
以下の例は論理演算式(条件判定)の一部に関数を使っています。

if (llGetListLength(items) < 10) {
  // リスト型変数itemsの要素数が10未満の場合
}

次の例も計算式の一部に関数を使っています。
文字列を連結する式ですね。

string msg = "Your name is " + llKey2Name(uuid);

このように、戻り値のある関数は自由に計算式の一部として使えます。

しかしながら戻り値のない関数は、単体で使うことになります。
例えばオーナーのみにメッセージを表示する関数llOwnerSayは、引数には表示する文字列を取りますが、戻り値はありません。

llOwnerSay("You got mail!");

戻り値がありませんので、代入や比較などはできません。

引数も戻り値もない関数の場合はもっとシンプルです。
例えば再生中のサウンドを止めるllStopSound関数には、引数も戻り値もありません。
このような関数を使うときには、

llStopSound();

単にこのように書くだけになります。

lslにはいろいろな関数が用意されていますが、どの関数がどのような引数、戻り値を持つのかについてはlslWikiなどのリファレンスを参照して下さい。
当blogのlsl関数・イベント一覧にも簡単ではありますが、関数と引数・戻り値の一覧があります。

ユーザー関数

さて、最後に・・・。
関数を自分で作る方法について説明しておきます。
あらかじめlslに用意された関数ではなく、自分で作った関数のことをユーザー関数と言います。

ユーザー関数は以下のような構文で記述します。

【戻り値・引数のあるユーザー関数】
戻り値の型 ユーザー関数名(引数1,引数2...){
  // 処理内容
  return 戻り値;
}


【戻り値のみがあるユーザー関数】
戻り値の型 ユーザー関数名(){
  // 処理内容
  return 戻り値;
}


【引数のみがあるユーザー関数】
ユーザー関数名(引数1,引数2...){
  // 処理内容
}


【引数も戻り値もないユーザー関数】
ユーザー関数名(){
  // 処理内容
}


具体的に例示しておきます。
以下は引数に「色データ」を渡すとprimの照明効果を指定された色にするユーザー関数です。
戻り値はありません。

light(vector color){
  llSetPrimitiveParams(
    [PRIM_POINT_LIGHT, TRUE, color, 0.5, 3.0, 0.75]
  );
}

次は戻り値のあるユーザー関数を見ておきましょう。
以下の例はサイコロ関数です。
引数には「サイコロの個数」と「サイコロの面数」を取ります。
戻り値はサイコロの出目の合計値です。

integer dice(integer num, integer face){
  integer i;
  integer ret = 0;
  for (i = 0;i < num; i++){
    ret += llFloor(llFrand(face)) + 1;
  }
  return ret;
}

llFrandという関数は、0から引数未満の範囲の小数値をランダムに返す関数です。
例えばfaceが6だったとすると、0.00~5.99999...の範囲の数値が返ってきます。
llFloor関数は小数部分を切り捨てる関数で、ランダムに返ってきた値を切り捨てて整数化します。
つまり、0.00~5.99999...の小数部分が切り捨てられるので、0~5の値になります。
サイコロの面は通常1から始まるので、+1して1~6の数値に直しています。

これをnumに指定された回数だけ繰り返し、値を変数retに加算していきます。
最後に、retrunでretの値を返しています。

ユーザー関数の使い方は普通の関数と同様です。
上記のサイコロ関数であれば、
if (dice(2,6) <= 9){
  // 6面サイコロ二個の値が9以下の場合
}

このように使うことができます。

ユーザー関数を使うメリットは、スクリプト内の複数個所で同じ処理を行うような場合に、処理内容を一箇所にまとめて書くことができる点です。
例えば、「スクリプト開始時」と「オブジェクトがrezされたとき」の両方のイベントで、「初期化」の処理をしたいような場合。
何も考えずに素直に書くと、以下のようになります。

default{
  state_entry(){
    llSetObjectName("Super Hogege Special");
    llSetObjectDesc("Sugoi Tsuyoi Kakkoii");
    llOwnerSay("Initialize OK!");
  }
  
  on_rez(integer param){
    llSetObjectName("Super Hogege Special");
    llSetObjectDesc("Sugoi Tsuyoi Kakkoii");
    llOwnerSay("Initialize OK!");
  }
}

二つのイベント内の処理がまったく同じになっています。
これでもスクリプトの動作としては間違ってはいませんが、あとから修正の必要が出てきた場合には両方を手直ししなければならなくなります。

そこで、ユーザー関数を使って二つの処理を一つにしてしまうと楽になります。

init(){
  llSetObjectName("Super Hogege Special");
  llSetObjectDesc("Sugoi Tsuyoi Kakkoii");
  llOwnerSay("Initialize OK!");
}

default{
  state_entry(){
    init();
  }
  
  on_rez(integer param){
    init();
  }
}

スクリプトの動きは先ほどとまったく変わりません。
ですがこちらのスクリプトでは、何か修正する際にはユーザー関数initの中身だけを直せばOKです。

以上、ちょっと駆け足でしたが関数についての話でした。

この記事へのトラックバックURL

http://miz.slmame.com/t8517
この記事へのコメント
詳しい講義をいつもありがとうございます。

>「愛人と名古屋のホテルに泊まる予定です」
笑いました! =)
Posted by sheila6225 Allen at 2007年06月09日 16:08