目次 はじめに †このドキュメントでは、FlightGear で利用されている Nasal というスクリプト言語について日本語で解説します。 Nasal とは †Nasal は Andy Ross 氏が開発したオブジェクト指向スクリプティグ言語で、 文法や言語仕様は perl や python, JavaScript のそれに似ています。 FlightGear では Nasal は標準のフライトモデルだけでは表せない機体の挙動や 電気系統の状態、装置・計器の動作を記述するのに広く用いられています。また、 チュートリアルやマルチプレーヤモード用のレースや山火事用を実現するために も用いられています。Nasal の特徴として Andy は以下の点を挙げています。
想定読者 †このドキュメントは、ある程度プログラミングに関して理解している方を想定し て記述されています。また、以下のFlightGear 開発者を対象としています。
Nasal の文法 †字句構造 †識別子 †Nasal の識別子は英数字及びアンダースコアで構成されています。識別子の最初の文字には数字は利用できません。 例: some_identifier nasalObjectIsLikeThis コメント †'#' で始まる行はコメントとしてみなされます。 例: # This is a comment 予約語 †以下の予約語は、変数名などの識別子として利用することはできません。 break else elsif for forindex foreach func if me parents print return size sprintf typeof while データ型、変数の型定義、及びリテラル †Nasal のデータ型にはスカラー(数値)、文字列、ベクター(配列)、ハッシュ、及び関数オブジェクトがあります。 クラスやオブジェクトもデータ型として存在しますが、これらはハッシュを利用して表現されています。 変数の明示的な型定義は必要なく、代入時の右辺値によって自動的に型が決定されます。 数値 (スカラー型) †数値としては、10進整数、小数、指数、及び16進整数の表現が可能です。 例: n1 = 3; # 10進整数 n2 = 3.14; # 小数 n3 = 6.023e23; # 指数 n4 = 0x123456; # 16進整数 文字列 (スカラー型) †文字列データはシングルクオート及びダブルクオートで囲まれた文字列として記述します。 ダブルクオート内では \ で始まる文字はエスケープシーケンスとして解釈されます。 これに対してシングルクオート内部では、シングルクオートそのもののエスケープシーケンス 以外はそのまま出力されます。以下の2つの文字列型データは同じ文字列を示します。 例: s1 = 'Andy\'s "computer" has a C:\righteous\newstuff directory.'; s2 = "Andy's \"computer\" has a C:\\righteous\\newstuff directory."; ベクター型(リスト/配列) †ベクター型データは鍵括弧([])に囲まれた、カンマ区切りの変数列となります。 例: list1 = ["a", "b", 1, 2]; ベクタ型データの要素を参照するには変数名の後のカギ括弧([])内に要素番号を記述します。 例: list1[0] # 上の例では "a" ハッシュ型(オブジェクト) †ハッシュ型は、キーと値とのペアを中括弧({})で囲んで表現します。各ペア間はカンマで区切られ、 キーと値とはコロン(:) で区切られ、キーと値のペア間はカンマで区切られます。 例: hash1 = { name : "Andy", job : "Hacker" }; EnglishEspanol = { "one" : "uno", "two" : "dos", "blue" : "azul" }; また、以下のような定義方法もあります。 例: hash1.name = "Andy"; # 変数.キー = 値 hash1["name"] = "Andy"; # 変数["キー"] = 値 リスト型と同様にハッシュの値を参照するには変数名の後のカギ括弧([])内にキーを指定するか、 ドット('.')に続けてキー名を指定します。 例: hash1["name"] # "Andy" hash1.name # "Andy" 上記の2つの記法はほぼ同じ意味を示しますが、異なる意味を持つ場合があります。それはハッシュの値が関数オブジェクトの場合です。次の例で示す2つのhash 参照は異なる動作結果になります。 例: hash = { sayHello : func(name) { print("Hello, " ~ name ~ "!"); me.sayBye() }, sayBye : func { print(" Bye!"); }} hash["sayHello"]("foo"); # (1) エラー hasn.sayHello("foo"); # (2) "Hello, Foo! Bye!" (1) の場合は関数コールと解釈されるため クラスのインスタンスを表す me を参照できません。従って me.sayBye() の呼び出し時に no such member エラーが発生します。これに対して (2) は hash オブジェクトの proc メソッドコールとして解釈されるので me 参照が可能となり、me.sayBye() が実行されます。 nil 型 †nil は上記のいずれにも属さない特殊な型です。通常は変数の値が未設定である事を示したり、関数のエラー値として返したりします。nil は他の型へは変換できないため、数値演算や文字列演算に nil を用いるとエラーになります。また、nil[0] や nil.key もエラーになります。関数の返り値が nil かどうかを判定する場合には == 演算子が利用可能です。 例: var property = getprop("/some/property"); if (property == nil) print("No such property"); else { # do something } ASCII コード定数 †バッククオートに囲まれた文字は 1バイトの ASCII コード(定数)を表します。 例: print(`A`); # 65 変数スコープ †Nasal では変数宣言の位置及び var という識別子の有無により変数のスコープ (参照可能な範囲)が異なります。変数のスコープには大きく分けて以下の3つが あります。 グローバルスコープ †var を付けずに関数やクラスの外で変数を定義するとグローバルスコープになり ます。グローバルスコープな変数は全スクリプトファイルで共有可能になります。 グローバルスコープ変数の使用は必要でない限り極力避けるべきです。なぜなら、 同名のグローバルスコープな変数が他のファイルに存在する場合は予期せぬ動作 をする可能性があるからです。通常グローバルスコープを利用するのは、多くの ファイルからそのまま再利用されるオブジェクトや関数です。 例: some_value = 15; # 他のファイルからでも参照可能 ファイルモジュールスコープ †関数やクラスの外に var をつけて変数を定義すると、単一ファイル内部で共有
可能ファイル内部からはそのまま参照可能。ファイル外からは拡張子を除いたフ
ァイル名をスコープとして明示します。 例: (some_module.nas ファイル内) var some_string = "you can use this in this file."; ファイル外から参照する場合の例 print(some_module.some_string); # ファイル名.変数名 で参照する ローカルスコープ †関数内部で var をつけて変数を定義すると、関数内部のみで利用可能なローカ ルスコープとなります。 例: var some_func = func { # some_func はファイルモジュールスコープ var string = "Local Scope"; # string は some_func 内部でのみ参照可能 print(string); # "Local Scope" と出力 } 関数内の変数のスコープについて †関数内で var をつけない場合は関数の外で宣言されている同名のグローバルス コープあるいはファイルモジュールスコープの変数を参照します。参照すべき変 数が存在しない場合は、undefined symbol エラーが発生します。 例1: var stringA = "File Module Scope"; var stringB = "File Module Scope"; var some_func = func { var stringA = "Local Scope"; # この関数内部でのみ参照可能 print(stringA); # "Local Scope" と出力 print(stringB); # "File Module Scope" と出力 } 例2: (エラーとなる場合) var some_func = func { string = "Some Message"; # エラー (string が関数外で定義されていない) } 基本的な文の記述 †Nasal での文は原則としてセミコロンで区切ります。また文の集まりであるクロージャは C や C++ と同様に {} で囲みます。 代入文: foo = bar; 関数オブジェクトの定義文: dummyFunc = func { var a = do_something(); return do_one_more_thing(a); } 演算子式 †代入 †代入式は以下の文法で表現されます。 <変数> = <式> ; 例: foo = bar; # foo に bar を代入 foo[0] = bar; # foo の最初の要素に bar を代入 foo.bar = baz; # foo に キー/値 = bar/baz というペアを代入する foo = [1, 2, 3]; # foo に 1, 2, 3 を要素にもつ配列を代入する foo = { name : "John", age : 34 }; # foo に name/John, age/34 なる要素をもつハッシュを代入する 四則演算 †演算子として +, -, *, / が利用可能です。 例: 1 + 20 * 3 / 4; 文字列の結合 †文字列を結合するには チルダ('~') 演算子を利用します。 例: var message = "Chao" print("The message is " ~ message); 数値と文字列の演算 †perl や Ruby と同様に Nasal では 数値と文字列とは同じスカラー型として扱われています。このため数値を示す文字列は数値として扱うことができます。また、数値を文字列として扱うことも可能です。文字列か数値かの解釈は演算子により決定されます。 例: print(1 + "2" ~ " dogs"); # "2" を数値として捉え 1 + 2 を計算。その後 # 結果の 3 を文字列と捉え、"3 dogs" と表示 print("1 + 3 = " + 4); # エラー; "1 + 3 = " を数値に変換できない print("1 + 3 = " ~ 4); # "1 + 3 = 4"; 全て文字列として扱われる 自己代入 †数値を持つ変数には自己代入として、 +=, -=, *=, /= が利用できます。 例: foo += 10; # foo = foo + 10; と等価 foo -= 10; # foo = foo - 10; と等価 また、文字列を持つ変数には ~= が利用できます。 例: var message = "Hi, "; message ~= "Nasal!"; # "Hi, Nasal!" 論理演算 †あるの? 算術演算 †算術ライブラリ関数により実現されています。 評価式 †評価式の演算子には ==, !=, >, <, >=, <= を また、評価式の論理演算には and 及び or を利用します。 例: if (a == b) { # a と b とが等価であることの評価 print("a is b."); } while (a >= b and a < 100) { # a >= b かつ a < 100 の間繰り返す。 a += b / 10; } # test_func() が 1 を返し、かつ、a > 10 または b < -10 の時に実行 if ((test_func() == 1) and (a > 10 or b < -10)) { # do something } 制御文 †for †perl と同様の for ループ記述となります。 例: for(var i=0; i < 3; i = i+1) { elem = list1[i]; doSomething(elem); } forindex †0..(ベクターの要素数-1) の間イテレーション(繰り返し処理)を行います for 文に似ていますが、上限をする必要はありません。 例: forindex(var i; list1) { doSomething(list1[i]); } foreach †ベクターから順に取り出した要素それぞれに対してイテレーション(繰り返し処理)を行います 例: list1 の各要素を elem に代入し doSomething(elem) を実行する foreach(elem; list1) { doSomething(elem) } if - else, if - elsif †例: if (a == b) return 0; elsif (a > b) return 1; else return 2; while †略 (perl や C とほぼ同じ) break †ループ制御から脱出します。 例: for (i=0; i < 10; i+=1) { if (list[i] == nil) break; # list[i] が nil ならループ終了 else { do_something(list[i]); } } Nasal ではループにラベルを付加することができます。 これにより break で抜け出す先を決定することができます。 例: doneWithInnerLoopEarly = dummyFunc; completelyDone = dummyFunc; for(OUTER; i=0; i<100; i = i+1) { # for 文に OUTER というラベルを付加 for(j=0; j<100; j = j+1) { if(doneWithInnerLoopEarly()) { break; # 内側のループから抜ける } elsif(completelyDone()) { break OUTER; # OUTER ラベルのある外側のループから抜ける } } } 関数 †Nasal の関数は全て関数オブジェクトとして表現されます。 関数オブジェクト †関数オブジェクトは func { <文>; } という形で表現されます。 例: 引数を明示しない関数オブジェクトの例: log_message = func { print(arg[0]); # arg[0] は第一引数 } log_message("test"); # "test" と出力される 引数を明示した関数オブジェクトの例: log_message = func(str, num = 0) { # 第2引数を省略すると num は 0 になる print(printf("1st argument = %s, value = %d", str, num); } log_message("test") # "1st argument = test, value = 0" と出力 クラス †クラスは hash オブジェクトとして定義します。クラスを定義するには以下の2通りの 方法があります。 クラスの定義方法1 †Class1 = {}; Class1.new = func { obj = { parents : [Class1], # Class1 オブジェクトの作成 (ハッシュオブジェクトで表現) # parents には自分も含む親クラスを階層下側から順にリストで記述する count : 0 }; # インスタンス変数 count に 0 を代入 return obj; # 新しい Class1 オブジェクトを返す } Class1.getcount = func { me.count = me.count + 1; # me は自身のオブジェクトを示す (C++ の this, python/ruby の self と同じ) return me.count; } c = Class1.new(); print(c.getcount(), "\n"); # prints 1 print(c.getcount(), "\n"); # prints 2 print(c.getcount(), "\n"); # prints 3 クラス定義方法2 †Class2 = { new : func { obj = {}; obj.parents = [Class2]; obj.count = 0; return obj; }, getcount : func { me.count = me.count + 1; return me.count; } }; この2つの表記は混ぜて記述しても問題ありません。しかしながら読みにくくな るので、混在した方がよい理由がない限り統一しましょう。 インスタンス変数 †インスタンス変数はクラスのメンバ変数のうち、インスタンス(オブジェクト)ごとに割り当てられた変数のことです。インスタンス変数は <クラス名>.<変数名>で参照します。 例: Class1 = { new: func { obj = { parents = [Class2] }; obj.some_value = 10; # インスタンス変数 return obj; } }; クラス変数 †クラス変数はクラスに属する変数で、クラス内外から参照可能です。クラス変数のスコープはクラスオブジェクト(クラスを表すハッシュ変数)のスコープに従います。クラス変数は<クラス名>.<変数名>で参照します。Nasal ではクラス定数もクラス変数として定義します。 例: Class1 = {}; Class1._instance = nil; # クラス変数 Class1.instance = func { if (Class1._instance == nil) Class1._instance = { parents : [Class1] }; return Class1._instance; } オブジェクト †クラスのインスタンスであるオブジェクトはクラスと同様にハッシュで表現します。 例: Class1 のオブジェクト obj を生成する var obj = { parents : [Class1], count = 1 }; 継承 †Nasal ではクラスとオブジェクトとの関連付けを、オブジェクト内の parents というインスタンス変数で表現しています。従って Nasal における継承も parents により決定されます。なお、Nasal での継承は、スーパークラスとサブクラスとの間の関係というよりも、クラスオブジェクトとインスタンスオブジェクトとの間の関係を意味する場合が多いです。 例: Class1, Class2 を継承するオブジェクトの作成 var obj = { parents : [Class1, Class2], count = 1 }; この例では Class1, Class2 を多重継承していますので、obj からは Class1, Class2 が持つメソッドを利用することができます。同名のメソッドが Class1, Class2 に存在する場合は、parents のリストの先頭から検索していくため、Class1 のメソッドが呼び出されます。上の例でClass2 のメソッドを呼び出したい場合は obj.parents[1].method という風に親クラスを明示してください。 動的継承 †クラスとオブジェクトとの関連付けは parents というインスタンス変数で表現するということを説明しました。この parents が指す値を変更することで、オブジェクトが継承するクラスを動的に変更することができます (オブジェクトがハッシュであることを思い出してください)。この際、すでに定義されたインスタンス変数はすべて保持されます。この動的継承を利用すれば、オブジェクトの動作を完全に別物にすることも可能です。このような動的継承は、状況に応じてオブジェクトの振る舞いやI/Fを変更したい場合に有効です。ただし、無意味な動的継承の乱用は混乱を招くだけなのでさけるべきでしょう。 例: var Class1 = {}; Class1.methodA = func { me.value = 1; print("Class1\n"); } var Class2 = {}; Class2.methodB = func { me.string = "2"; print("Class2\n"); } var obj = { parents : [Class1] }; # まずは Class1 に属すオブジェクトを生成 obj.methodA(); # "Class1" obj.parents = [Class2]; # 親クラスを Class2 に変更 obj.methodB(); # "Class2" print(obj.value + obj.string); # 3 (me.value も me.string も保持する) 組み込みオブジェクト/関数 †そのうち記述予定 :-p FlightGear 用ライブラリ †クラス †Node †そのうち記述予定 :-p オブジェクト †props †FlightGear のプロパティを表すオブジェクトです。FlightGear のプロパティツリーにアクセスする際には props.globals.getNode("/path/to/property") という風に記述します。 例: エンジン[0] のプロパティを取得する var rpm = props.globals.getNode("/engines/engine[0]/rpm"); screen †FlightGear の画面を表すオブジェクトです。 このオブジェクトの典型的な使い方は画面にメッセージを表示することです。 例: 画面にメッセージを表示する screen.log.write("Howdy?"); 関数 †fgcommand(command, node) †FlightGear のコマンドを呼び出します。command には "show-dialog" などのコマンド文字列を指定します。 node には プロパティノードオブジェクトを指定します。このオブジェクトには、 props.Node.newで作成した、 あるいは props.globals.getNode() で取得したオブジェクトを指定します。 例: FlightGear のダイアログ表示関数 var showDialog = func(dialogName) { setprop("/nasal/tmp/dialog-args/dialog-name", dialogName); fgcommand("dialog-show", props.globals.getNode("/nasal/tmp/dialog-args")); } getprop †FlightGear のプロパティツリーの値を取得します。指定されたプロパティが 存在しない場合は nil を返します。 例: コールサインを取得する var callsign = getprop("/sim/multiplay/callsign"); interpolate(property, value, interval) †FlightGear のプロパティの値を、現在の値から value まで interval で指定された時間を掛けて スムーズに変更します。ランディングギアの位置や計器の針の位置など、徐々に値を変更したい時に 利用すると便利です。 例: interpolate("/some/property", 10, 5); # 5秒間かけて /some/property の値を現在の値から10に変更する interpolate(props.globals.getNode("some/node"), 10, 5); # 5秒間かけて /some/node の値を現在の値から10 に変更する print(string) †コンソールに文字列を出力します。 例: print("message."); print(sprintf("some value = %d", value)); setprop †FlightGear のプロパティツリーの値を設定します。 例: setprop("/some/property", 10); setprop("/some/string", "message"); setprop("/some/property, 1.45); settimer †指定された時間が経過したら、関数を呼び出します。時間に0を指定すると次のフレーム描画時に 関数を呼び出します。 例: settimer(func { print("tick") }, 1); # 1秒後に関数オブジェクトを実行 settimer(func { obj.method() }, 0); # 次のフレームで obj.method() を実行 settimer(proc(), 0); # 次のフレームで proc という関数を実行 一定期間おきに実行される処理を記述する場合は、呼び出された関数内で更に settimer を呼び出します。 例: 1秒おきに timerFunc を実行する処理 var timerFunc = func { do_something(); settimer(timerFunc, 1); } settimer(timerFunc, 1); setlistener †FlightGear のプロパティが変更された際に処理が実行されるように指定します。 例: /engines/engine/rpm の値が変更されたら changeRPM() 関数を実行する setlistener("/engines/engine/rpm", changeRPM); setlistener("/engines/engine/rpm", func { changeRPM(); }); rand †0以上1未満の乱数を発生させます。 Nasal の落とし穴 †関数オブジェクト内の me は実行時に決定される †クラスメソッド内からコールバック関数として関数オブジェクトを渡した場合は、 関数オブジェクト内のコードを実行する際のスコープに気をつける必要があります。 とくにクラス内でのオブジェクトを表す me がどのオブジェクトを示すかを 知っておかなければなりません。下の例では、ClassB.testCallback() を呼び出すと "callback" と表示されることを期待しますが、実際には "wrong callback" が表示されます。 例: me が指すオブジェクトが変わる場合 ClassA.register = func(proc) { me.proc = proc; } ClassA.callme = func { me.proc(); } ClassA.callback = func { print("wrong callback"); } ClassB.testCallback = func { var a = ClassA.new(); a.register(func { me.callback(); } a.callme(); } ClassB.callback = func { print("callback"); } b = ClassB.new(); b.testCallback(); # "wrong callback" と出力される これは、func { me.callback(); } の me が実行時のスコープに依存するためです。 me.callback() が呼び出される順序を考えていくとこの意味がわかります。
関数オブジェクトが呼び出される時点(5)では me の値は b ではなくa になっています。 me の値は関数オブジェクトを呼び出した側の me が指す値になるからです。 このようなデバッグしにくい現象を避けるためには、このことに留意する必要があります。 コールバック関数内で me を利用しない †現在のオブジェクトを示す me 以外ではこの現状は起きません。 以下のように書き換えることで、期待どおり ClassB.callback() が呼び出されます。 例: me を self で置き換える ClassB.testCallback = func { var self = me a.register(func { self.callback(); }); a.callme(); # self = b なので ClassB.callback() が呼び出される } 呼び出し側で me の値が示すオブジェクトを明示する call 関数を利用する~ †call 関数では、呼び出す関数オブジェクト内での me の値を明示することができます。 これを利用すれば、関数オブジェクト内での me を呼び出される側のオブジェクトに指定できます。 従って関数オブジェクト内から指定されたオブジェクトの関数を呼び出すことができます。 例: (ライブラリ関数 call を利用して、関数オブジェクト呼び出し時の me を明示する) ClassA.register = func(proc, callee) { me.proc = proc; me.callee = callee; } ClassA.callme = func { call(me.proc, [], me.callee); } ClassB.testCallback = func { var a = ClassA.new(); a.register(func { me.callback(); }, me); a.callme(); # me = a.me.callee = b なので b.callback() が呼び出される } 変更履歴 †
|