オブジェクトとクロージャ

このごろの言語はオブジェクト指向でなければ 言語として認めてもらえないような気配もあったりするので、 crowbarにもオブジェクトを組み込むことにします。

が、crowbarのオブジェクト指向は、 C++やJavaのそれとはちょっとばかり趣が違います。 なにしろクラスがありません。

では具体的にどんな仕様と実装になっているのか、今回はそれを説明します。

GLOBALをかましたソースは こちらから参照可能です。

ダウンロードは、UNIX版がこちら、 Windows版がこちら

細かい不具合修正を全バージョンに対して行うのは大変ですので、 ver.0.3系列はver.0.3.01に統合しました。 ここから参照してください。

オブジェクト

crowbarでは、配列の生成はネイティブ関数new_array()を使いますが、 同様に、オブジェクトはネイティブ関数new_object()を使用します。

 o = new_object();

Cの構造体と同じように、crowbarのオブジェクトはメンバを持ちます。 ただ、型の宣言がないので、メンバは、代入により実行時に追加されます。

 o = new_object();
 o.hoge = 10;
 o.piyo = 20;
 print("o.hoge.." + o.hoge + "o.piyo.." + o.piyo + "\n");

型宣言がないのは気持ちが悪い、 という人もいるでしょうが(いやその私もそのひとりなのですが)、 ひとまずこれで、Cの構造体に似たことは実現できます。

なお、Javaなどと同様、オブジェクトは参照型であり、 new_object()で返されるのはオブジェクトへの参照です。 よって、以下のコードは20を表示します。

  o1 = new_object();
  o1.hoge = 10;
  o2 = o1;
  o2.hoge = 20;
  print(o1.hoge);

「オブジェクト」という用語について

前回のGCの説明では、 文字列や配列といった参照型のデータを、 まとめて「オブジェクト」と呼んでいました。 そして、これらの型を表現するために、 CRB_Objectという構造体が導入されています。

が、今回は、new_object()で生成されるモノのことを指して 「オブジェクト」と呼んでいます。

このあたり、用語が混乱しているわけですが、 他に適切な言葉が思いつかなかったもので――すみません。

というわけで、用語が混乱しているのは素直に認めるとして、 今後どのように言葉を使い分けていくかですが、

ということにしようと思います。

では、実装者側は、「new_object()で生成される構造体のようなもの」を 何と呼べば良いのか、ということになりますが、 ここでは「assoc」と呼ぶことにします。 「assoc」というのは連想配列(associative array)の略です。 文字列をキーに値が取り出せる、という点において、 crowbarのオブジェクトは結局のところ連想配列だからです ※1

オブジェクトの実装について

オブジェクト(実装者用語のassoc)は、 参照型ですから、配列や文字列と同様、 CRB_Objectの共用体のメンバとして保持します。 当然、対応する列挙型も必要です。

typedef enum {
    ARRAY_OBJECT = 1,
    STRING_OBJECT,
    ASSOC_OBJECT,
    SCOPE_CHAIN_OBJECT,
    OBJECT_TYPE_COUNT_PLUS_1
} ObjectType;

…

struct CRB_Object_tag {
    ObjectType  type;
    unsigned int        marked:1;
    union {
        CRB_Array       array;
        CRB_String      string;
        CRB_Assoc       assoc;
        ScopeChain      scope_chain;
    } u;
    struct CRB_Object_tag *prev;
    struct CRB_Object_tag *next;
};

なにやら「SCOPE_CHAIN」とかいうのも増えていますが、これについては後述。

そして、CRB_Assocそのものの定義は、以下のようになります。

typedef struct {
    char        *name;
    CRB_Value   value;
} AssocMember;

struct CRB_Assoc_tag {
    int         member_count;
    AssocMember *member;
};

なんのことはありません。「メンバ」というのは名前と値の組で、 assocは、それを可変長配列で保持している、というだけです。

ちなみに現状の実装では、メンバが追加される度に realloc()でひとつずつ要素を増やしていますし、 新規要素は可変長配列の末尾に追加されます。 検索はリニアサーチです(ま、富豪的プログラミングということで)。

assocを操作する関数群はheap.cに用意されており、 以下のものがあります。

/* assocの生成 */
CRB_Object *crb_create_assoc(CRB_Interpreter *inter);
/* assocにメンバを追加 */
void crb_add_assoc_member(CRB_Interpreter *inter, CRB_Object *assoc,
                          char *name, CRB_Value *value);
/* assocのメンバ検索。存在しなければNULLを返す */
CRB_Value *crb_search_assoc_member(CRB_Object *assoc, char *member_name);

当然ですが、assocが追加されたことで、GCも修正する必要があります。 GCはmarkフェーズにおいて、assocのメンバを順に辿ります。

クロージャ

オブジェクトと言うのならデータメンバだけじゃなくメソッドも要るだろう、 メソッドはどうした、という声が聞こえてきそうですが、 それはしばらく放置して、別の話を進めます。

crowbarではクロージャというものを使うことができます。 クロージャとは、式の中で定義できる関数のようなものです。

  # クロージャの生成
  c = closure(a) {
      print("a.." + a);
  }

  # クロージャの呼び出し
  c(10);

「closure」というのは、クロージャ生成のための予約語です。 この後ろに、括弧で囲んだ仮引数とブロックを書くことで、 クロージャが生成され、ここではそれをcに代入しています。 「c(10)」のように書くことで、それを呼び出すことができます。 よって、このコードは、「a..10」と表示します。

Cプログラマなら、これを見れば、「なんだ、関数ポインタのようなものか」 と思うでしょう(まあ、クロージャには、 式の中でひょっこり書けるという手軽さはありますが)。 クロージャは、確かに関数ポインタによく似た側面がありますし、 実際、同じような使い方もします。

ただし、決定的に違うのは、クロージャは、 クロージャが生成された個所のローカル変数を参照できる、ということです。

一例として、foreachについて考えてみます。 crowbarで配列の全要素についてループしたければ以下のように書きますが

  for (i = 0; i < array.size(); i++) {
      # 処理
  }

この書き方は、 それが配列であるということに依存した実装になってしまっています。 気が変わって配列ではなく連結リストにしたら、 このような記述をしている個所すべてを直さなければなりません。 それは嫌だということで、たとえばC#では、 foreachという構文が用意されています。

  foreach (Object o in hogeCollection)
  {
      // 処理
  }

Javaでも、J2SE5.0から同種の構文が追加されました。

この構文は確かに便利ではありますが、 便利だからといって構文規則にまで手を加えるのはいかがなものか、 という考え方もあるでしょう。 しかし、クロージャが使える言語なら、 たとえば以下のように書けるわけです (現状のcrowbarでこう書けるわけではないので注意)。

  foreach(hoge_collection, closure(o) {
      # 処理
  });

このforeachは予約語ではなく、単なるライブラリ関数です。 第1引数にコレクションのオブジェクトを、 第2引数にクロージャを受け取っています。 foreach関数が、コレクションに格納されている要素を順に取り出し、 それを引数として、第2引数で渡されたクロージャを呼び出してくれるわけです。

単にforeach関数から呼び出してもらうだけなら、 Cの関数ポインタでも実現は可能でしょう。 しかし、このような使い方をするなら、ループの内側において、 外側のローカル変数を参照したいと思うのが普通ではないでしょうか。

  fp = fopen("hoge.txt", "w");
  foreach(hoge_collection, closure(o) {
      fputs(o.name, fp); # ループの外側の変数fpを参照
  });

クロージャではそれが可能であり、その点が、 Cの関数ポインタとの決定的な違いであるわけです。

メソッド

さて、オブジェクトとクロージャを組み合わせると、 以下のような書き方もできることになります。

  1:  # 「点」を生成する関数(コンストラクタ)
  2:  function create_point(x, y) {
  3:      this = new_object();
  4:      this.x = x;
  5:      this.y = y;
  6:  
  7:      # 座標を表示するメソッドprint()の定義
  8:      this.print = closure() {
  9:          print("(" + this.x + ", " + this.y + ")\n");
 10:      };
 11:  
 12:      # 移動するメソッドmove()の定義
 13:      this.move = closure(x_vec, y_vec) {
 14:          this.x = this.x + x_vec;
 15:          this.y = this.y + y_vec;
 16:      };
 17:      return this;
 18:  }
 19:  
 20:  # オブジェクトの生成
 21:  p = create_point(10, 20);
 22:  
 23:  # move()メソッドの呼び出し
 24:  p.move(5, 3);
 25:  
 26:  # print()メソッドの呼び出し
 27:  p.print();

crowbarには特に「メソッド」という機能はないのですが、 オブジェクトのメンバにクロージャを格納することで、 見掛け上、JavaやC++のメソッドと似たようなものを実現できます。

上記の「this」は予約語でも何でもありません。 単なる変数ですので、どんな名前でもよいのですが、 JavaやC++に慣れた人にはthisがわかりやすいのではないでしょうか。 ポイントは、クロージャはその外側のローカル変数を参照できるので、 print()やmove()の内部からthisが参照できる、ということです。

また、ポリモルフィズムがしたければ、「サブクラス」にて、 「スーパークラス」のメソッドを上書きすればよいわけです。 もちろんcrowbarにはそもそもクラスがないので、 「スーパークラス」も「サブクラス」も、 使う側で決めるものでしかないのですけれど。

function create_extended_point(x, y) {
    this = create_point(x, y);

    # print()をオーバーライド
    this.print = closure() {
        print("**override** (" + this.x + ", " + this.y + ")\n");
    };

    return this;
}

カプセル化が欲しければ、xやyをthisに格納せずに、 以下のようなget_x()やget_y()を書けばよいでしょう。 この場所のクロージャからは、引数、 すなわちローカル変数であるxやyが参照できるからです。

function create_point(x, y) {
    this = new_object();

    this.print = closure() {
        print("(" + x + ", " + y + ")\n");
    };

    this.move = closure(x_vec, y_vec) {
        x = x + x_vec;
        y = y + y_vec;
    };

    # xのgetter
    this.get_x = closure() {
        return x;
    };

    # yのgetter
    this.get_y = closure() {
        return y;
    };

    return this;
}

これが、crowbarにおけるOOサポートです。

クロージャの実装について

上のリストを見て、こんな疑問を持った人もいるのではないでしょうか。

thisやらxやらyやらはローカル変数なんだから、 関数create_point()を抜けた時点で解放されてしまうだろう。

いくらクロージャの中からその外側のローカル変数が参照できるとはいっても、 なくなってしまったものは参照できないのでは?

――もっともな疑問ですが、これがそうならないのがクロージャの面白いところです。

Cなどでは、ローカル変数の領域は、関数に入った時点でスタック上に確保され、 関数を抜けたタイミングで解放されます。 この時、確保/解放されるひとかたまりのメモリをフレームなどと呼びます。

crowbarでも、ver.0.2までは本質的に同じです (フレームをスタックではなくヒープに確保する、というだけで)。 ところが上の例において、print()やmove()といったメソッドは、 create_point()がとっくに終了した後で呼び出され、 しかもその中でthisなどを参照しています。 今までと同じように、「関数を抜けたらフレームは解放される」 という規則では、これに対応できません。

現状のcrowbarでは、 フレームが確保されるタイミングは従来通りですが、 解放されるタイミングは、「関数を抜けた時」ではなく、 「フレームへの参照がなくなった時」です。 つまり、フレームの解放はGCが行ないます。

では、実際の実装方法を考えてみましょう。 まず、フレームは、1回の関数呼び出し分のローカル変数群の格納場所ですが、 ローカル変数群というのは、 要は変数名とその値が複数組み合わさったものですから、 今ならassocが使えます。 つまり、関数呼び出しのタイミングで、ひとつのassocが生成され、 ローカル変数はそこに格納されることになるわけです。

そしてクロージャですが、クロージャは、関数ポインタと似ていますが、 それが生成された個所のローカル変数を参照できるという特徴があります。 ここで、クロージャの「生成」と呼んでいるのは、 以下のような、予約語closureを使ったクロージャの定義が、 実行されたタイミングを指します。

  this.print = closure() {
      print("(" + this.x + ", " + this.y + ")\n");
  };

closure以下の記述が「クロージャ」という値を生成し、 それをthis.printに代入しています。 クロージャは値ですから、CRB_Valueに格納できなければなりません。 よって、CRB_Valueの共用体定義の中に、 CRB_Closureを追加する必要があります。

typedef enum {
    CRB_BOOLEAN_VALUE = 1,
    CRB_INT_VALUE,
    CRB_DOUBLE_VALUE,
    CRB_STRING_VALUE,
    CRB_NATIVE_POINTER_VALUE,
    CRB_NULL_VALUE,
    CRB_ARRAY_VALUE,
    CRB_ASSOC_VALUE,
    CRB_CLOSURE_VALUE,
    CRB_FAKE_METHOD_VALUE
} CRB_ValueType;
…
typedef struct {
    CRB_ValueType	type;
    union {
	CRB_Boolean	boolean_value;
	int		int_value;
	double		double_value;
	void		*native_pointer_value;
	CRB_Object	*object;
	CRB_Closure	closure;
	CRB_FakeMethod	fake_method;
    } u;
} CRB_Value;

なにやら「FAKE_METHOD」とかいうのも増えてますが、 これについては後述(こんなんばっか)。

そして、そのCRB_Closureの定義ですが、 クロージャは、関数ポインタと、それが生成された個所のフレームが参照できる、 という定義からすれば、以下のようになりそうな気がします。

typedef struct {
    CRB_FunctionDefinition *function;
    CRB_Object          *environment; /* フレームのassocを指す */
} CRB_Closure;

メンバfunctionは、関数定義の実体であるCRB_FunctionDefinitionを指します。 そしてenvironment※2は、 クロージャが生成された個所のフレームを指す ――ということになりそうな気がしますが、 ここでもうひとつ考慮しなければならないことがあります。 それは、クロージャはネストができる、ということです。

  1:  function f() {
  2:      a = 10;
  3:      c1 = closure() {
  4:          b = 20;
  5:          c2 = closure() {
  6:              # ここからはa, bの両方を参照したい
  7:              print("a.." + a + "\n");
  8:              print("b.." + b + "\n");
  9:          };
 10:          c2();
 11:      };
 12:      return c1;
 13:  }
 14:  
 15:  c = f();
 16:  c();

上のコードで、内側のクロージャの内部からは、 ローカル変数a, bの両方が参照できなければならないでしょう。

この例で言えば、まず15行目の呼び出しにより f()が呼び出された時点でフレームがひとつ生成され、 aはそのフレームに格納されます。 そして、3〜11行目までで、クロージャc1が生成され、 f()はそのクロージャを返します。 まだc1は実行されていないので、クロージャc2は生成されていません。

次に、16行目の呼び出しにより、クロージャc1が呼び出され、 この呼び出しによりもうひとつのフレームが生成されます。 bはこちらのフレームに格納されることになります。 そして、クロージャc2が生成されるわけですが、 この中からは、 別々のフレームに格納されているaとbを両方とも参照したいわけです。

頭がこんがらがってきそうですが、ひとまず言えることは、 「ローカル変数を参照する際は、 ひとつのフレームからだけ探せばよいわけではなく、 複数のフレームを順に辿って検索しなければならない」 ということです。

そこで、スコープチェーンという概念を導入します。 スコープチェーンとは、 フレームとなるassocを連結リストで管理するためのものです。

この連結リストを構築するために、 ScopeChainオブジェクトを導入します。 ScopeChainオブジェクトもGCの対象としたいので、 CRB_Objectの共用体のメンバとします(上の方で 後述すると言ったうちのひとつがこれ)。 ScopeChain構造体の定義は以下の通りです。

typedef struct {
    CRB_Object  *frame; /* CRB_Assocを指す */
    CRB_Object  *next;  /* ScopeChainを指す */
} ScopeChain;

そして、CRB_Closureは、フレームのassocを直接指すのではなく、 ScopeChainを指すことになります。

typedef struct {
    CRB_FunctionDefinition *function;
    CRB_Object          *environment; /* ScopeChainを指す */
} CRB_Closure;

いやそのコメント以外何も変わっていませんが ※3

また、LocalEnvironment構造体も、同様にScopeChainを指すことになります。

これを具体的にどのように使うのかは、 実際の動きを追いながら考えるのがわかりやすいでしょう。

実際の動きを追ってみる

というわけで、上で挙げたクロージャのネストのサンプルをベースに、 実際にクロージャがどのように生成され、 スコープがどのように変化していくのかを追ってみましょう。 一応リストを再掲します。 これが本ならページ稼ぎとか紙資源の無駄とか言われるところですが、 Webだと、量をほとんど気にしなくてよいところがよいですな。

  1:  function f() {
  2:      a = 10;
  3:      c1 = closure() {
  4:          b = 20;
  5:          c2 = closure() {
  6:              # ここからはa, bの両方を参照したい
  7:              print("a.." + a + "\n");
  8:              print("b.." + b + "\n");
  9:          };
 10:          c2();
 11:      };
 12:      return c1;
 13:  }
 14:  
 15:  c = f();
 16:  c();
1.普通の関数呼び出し

まず、15行目の呼び出しによりf()が呼び出された時点で、 LocalEnvironmentがひとつ用意され、 その先にフレームが生成されます。 このあたりの動きは従来の関数呼び出しと同じと言ってよいでしょう。

なお、図を単純にするために、図中にはScopeChainオブジェクトは登場しません。 フレームのassocが単独で連結リストを構築できるかのように描いています。

2.クロージャの生成

次に、3〜11行目でクロージャが生成されます。

クロージャは、 生成された時点でLocalEnvironmentが保持しているスコープチェーンへの 参照を保持します(規則1)。

なお、c1自体ローカル変数ですから、aと同じフレームに格納されるのですが、 図を単純にするためこれも省略します。

3.クロージャの呼び出し

次に、16行目でクロージャc1が呼び出されます。

この時も、通常の関数呼び出しと同様、 新たなLocalEnvironmentとフレームを作るのですが、 この時、新規作成したフレームの後ろに、 クロージャが保持していたスコープチェーンを連結します(規則2)。

ローカル変数を検索する際は、 LocalEnvironmentが参照しているスコープチェーンを順に辿って検索します。 これにより、c1の中で、ローカル変数aが参照できるわけです。

4.クロージャの中でのクロージャの生成

5〜9行目で、ふたつめのクロージャc2を生成しています。

この手順は前回と同様で、 クロージャは、 生成された時点でLocalEnvironmentが保持しているスコープチェーンへの 参照を保持します(規則1)から、 c2は、bが格納されたフレーム、 aが格納されたフレームを連ねたスコープチェーンを保持することになります。

5.ネストしたクロージャの呼び出し

そして、10行目でc2が呼び出された際には、 規則2により、 c2が参照していたスコープチェーンが新しいLocalEnvironmentに連結されますから、 c2の中からはaもbも両方とも参照することができるわけです。

構文規則

オブジェクトとクロージャの実現のため、 構文規則を以下のように変更しています。

postfix_expression
        : primary_expression
        | postfix_expression LB expression RB
        | postfix_expression DOT IDENTIFIER /* メンバ参照 */
        | postfix_expression LP argument_list RP /* 関数呼び出し */
        | postfix_expression LP RP /* 引数なしの関数呼び出し */
        | postfix_expression INCREMENT
        | postfix_expression DECREMENT
        ;

オブジェクトのメンバ参照を、新たにpostfix_expressionとして追加しています。 また、関数呼び出しについては、従来は以下のように定義されていましたが、

primary_expression
        : IDENTIFIER LP argument_list RP
        | IDENTIFIER LP RP
	;

クロージャの導入により、呼び出し対象がIDENTIFIERとは限らなくなったので、 postfix_expressionに引越してきています。

また、クロージャの生成に関する構文は以下の通りです。

closure_definition
        : CLOSURE IDENTIFIER LP parameter_list RP block
        | CLOSURE IDENTIFIER LP RP block
        | CLOSURE LP parameter_list RP block
        | CLOSURE LP RP block
        ;

引数のありなしで2通り、 およびclosureの後ろに名前を付けるかどうかで2通り、 2×2の4通りの構文が定義されています。

ここまでの例では、クロージャには名前を付けていませんでした。 名前付きのクロージャはどのようなケースで使うのかといえば、 クロージャの中で自分自身を再帰呼び出ししたいケースです。

実装上は、名前付きのクロージャは、 そのクロージャが呼び出されて新しいフレームが生成された時点で、 クロージャ自身を、 その名前のローカル変数として登録することで実現しています。

上記closrue_definitionから、create.cにて以下の構造体が作成されます。

typedef struct {
    CRB_FunctionDefinition *function_definition;
} ClosureExpression;

クロージャ定義からはCRB_FunctionDefinitionが構築されるわけですが、 そのCRB_FunctionDefinitionは、CRB_Interpreterのfunction_listにはつながれません。 解析木のClosureExpressionから参照されているだけです。

普通の関数

構文規則の項でも説明したように、ver.0.2までのcrowbarでは、 関数呼び出しは以下のように定義されていましたが、

primary_expression
        : IDENTIFIER LP argument_list RP
        | IDENTIFIER LP RP
        ;

今のcrowbarでは、関数呼び出しを示す()の左側には、 IDENTIFIERに限らず任意の式を書くことができるようになりました。

そのため、普通の関数も、関数名がクロージャを返すようになっています。 たとえば「print("hello\n")」という呼び出しは、 「print」という識別子がクロージャを返し、それに対する呼び出し、 という形で動作します。このとき、printが返すクロージャの environmentメンバはNULLになっています。

よって、

  p = print;
  p("hello, world.\n");

というように、普通の関数を変数に代入することも可能です。

メソッドもどき

crowbarでは、たとえば配列にはsize()という「メソッドのようなもの」 が付いていました。

さて、これをどう実現するかですが、 他の関数と同じように、クロージャを返すようにすると、 配列のsize()に対応する関数を、 ネイティブ関数なり何なりで作らなければなりません。 まあ、作るのは作ればよいのですが、 現状のCRB_Closureには、「array.size()」と呼び出した際、 arrayへの参照を保持する場所がありませんから、 呼び出されても配列のサイズを返すことはできません。

そこで、CRB_Valueの共用体に、 「メソッドもどき」専用CRB_FakeMethodというメンバを追加することにしました (上の方で後述すると言ったうちのもうひとつがこれ)。

typedef struct {
    char        *method_name; /* メソッド名 */
    CRB_Object  *object;      /* 「this」に相当するオブジェクト */
} CRB_FakeMethod;

関数呼び出しの際、対象がクロージャでなく「メソッドもどき」の場合は、 ver.0.2と同じように、ソース埋め込みでベタに処理しています。

白状すると

「クロージャ」という機能は、Lispなどの関数型言語ではよく使われます。 PerlやRubyやPythonやJavaScriptにも存在します。 ――が、私はこれらの言語をほとんど使ったことがないので、 クロージャの「使いどころ」はおそらく身に付いてないと思います。

また、crowbarのオブジェクトは、クラスを元に生成されるのではなく、 インスタンスごとに異なるデータメンバやメソッドを持つことができます。 このような言語を「プロトタイプベース」と呼ぶのですが (現状のcrowbarにはプロトタイプチェーンがないので、 そう呼んでよいかすら疑問ですが…)、 私は、 プロトタイプベースのオブジェクト指向言語も ろくに使ったことはありません(JavaScriptで少々試した程度)。

経験が浅いからこそ、作ってみることで理解してみよう、 という目的もあったわけですが、 そもそも間違った理解をしている可能性もあります。 間違いを見付けた方は、どうぞ御指摘くださいませ(_o_)

今後のこと

ここまでで、言語のコアとしての機能はかなり揃ってきたと思うのですが、 割と闇雲に作ってきたため、いろいろいびつなところもあります。

以後の拡張(というか仕様のバグフィックス?)は、 だいたいこんなところかなあ、と今のところ思っています。

それではまた気長にお待ちくださいませ。

参考URL



前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る