LoginSignup
16
8

More than 1 year has passed since last update.

J言語のオブジェクト指向について

Last updated at Posted at 2014-07-19

プログラミング言語Jはオブジェクト指向プログラミングをサポートしています。具体的には継承と多態性があります。

実行時に名前空間を生成して@ISAを設定するという、JavaScriptとPerl5を足して焼酎で割ったような変わった仕組みです。

この記事で使うJ用語

J言語は用語がちょっと変わっています。いくつか機能的に似ているものを並べます。

J用語 日本語 他の言語で言うところのあれ
Verb 動詞 関数、演算子、メソッド +echo
Noun 名詞 定数、データ、リテラル 1 2 3, 'literal'
Locale ロケール、場所 名前空間、スコープ、モジュール、クラス、オブジェクト base, z, <'0', <'1', <'2'..
Copula 連結詞 代入演算子 =: (グローバル), =. (関数ローカル)
Name 名前 変数 foo, x, y

基本的な文法

  • クラスの定義はcoclass 'クラス名'で始めます。それ以後に書いたものが全てメンバーになります。ファイルが終了すると、クラスの定義も終わります。1ファイルで複数クラスを定義することもできます。

  • インスタンスの生成はパラメータ conew 'クラス名'という形で行います。インスタンスの生成後、左辺のパラメータがクラスのcreateというメソッドに渡されます。これがコンストラクタにあたります。

  • メソッドの呼び出しは メソッド名__オブジェクト名 引数 という文法です。

  • インスタンス変数の代入は、クラス内部では 変数名 := 値 という文法です。クラス外部では 変数名__オブジェクト名 := 値 という文法です。

  • メソッドの定義は メソッド名 := 動詞 という文法です。

カウンタの例

早速カウンターを作ってみます。(信じ難いことに、この言語のコメント行はNB.で始まります)

数取器

counter.ijs
NB. クラス名の設定 (以後宣言する変数はすべてこのクラスに属す)
coclass 'Counter'

NB. コンストラクタはcreateという名前で定義する。初期値は引数(y)で受け取る
create =: verb define
  count =: y
)

NB. カウントを増やすメソッド
tally =: verb define
  count =: count + 1
)

NB. カウントを0にリセットするメソッド
reset =: verb define
  count =: 0
)

NB. 現在の値を表示するメソッド
report =: verb define
  echo count
)

作ったクラスは、次のように利用します。

main.ijs
NB. 別ファイルの読み込み
require 'counter.ijs'

NB. インスタンスの生成
hoge =. 1337 conew 'Counter'

NB. メソッド呼び出し
report__hoge '' NB. 1337 と表示
tally__hoge ''
report__hoge '' NB. 1338 と表示
tally__hoge ''
report__hoge '' NB. 1339 と表示
tally__hoge ''
report__hoge '' NB. 1340 と表示
reset__hoge ''
report__hoge '' NB. 0 と表示
tally__hoge ''
report__hoge '' NB. 1 と表示

説明

各所にくっついている''はイディオムです。Jでは引数のない関数が作れないのでダミーの引数を与えています。''はただの空文字列(=空配列と同義)です。

他の言語でいうself.this->のように、インスタンス変数とローカル変数を区別する文法はありません。ローカル変数とインスタンス変数が両方あるときは、ローカル変数が参照されます。

ロケールについて

J言語にはロケールという仕組みがあります。これはネームスペースやパッケージにあたるものです。

ロケールは、変数を束ねるものです。各ロケールはユニークな名前を持ちます。

カレントロケール

変数名を単独(/[a-zA-Z]*[a-zA-Z]/)で読み書きするとき、現在アクティブになっているロケールが名前解決に使われます。これを暗黙ロケールやカレントロケールと言います

カレントロケールはcocurrent 'ロケール名'で切り替えることができます。カレントロケールはconame ''で取得できます。

ロケールの切り替え.ijs
NB. foo ロケールに切り替え
cocurrent 'foo'

echo coname '' NB. foo と表示される
a =: 42

NB. bar ロケールに切り替え
cocurrent 'bar'
echo coname '' NB. bar と表示される

a =: 54
echo a_foo_ NB. 42 と表示される
echo a NB. 54 と表示される

ロケールは、一つのファイルの中で何度でも切り替えることが出来ます。他スクリプトを読み込む命令は、内部でのロケールの変更を打ち消すようになっているようです。PythonのモジュールというよりはPerlのパッケージに近い動作です。

=:の代わりに=.で定義した変数はその動詞の定義内だけで有効な変数になります。これを使って定義した変数は動詞の実行後破棄されます。

他ロケールの参照

他のロケールに属す変数を参照するには、変数名_ロケール名_のように変数名のあとにロケール名を修飾した形で書きます。この書き方をExplicit locative name(逐語訳: 明示的場所格名称)と言います。

名前_ロケール名_のほかに、名前__変数名という書式があります。こちらはObject locative name(逐語訳: 対象的場所格名称)といいます。この書式では、変数名に記憶されたロケール名が参照されます。Perl風に書くと $hoge__fuga${'hoge_' . $fuga . '_'}のシンタックスシュガーになる感じでしょうか。つまり、参照するロケールを実行時に指定できます。

他のロケールに属す動詞を呼び出す際には、カレントロケールが一旦切り替わって実行されます。

特にアクセス制御や名前隠蔽の仕組みはなく、外から代入もできます。変数はすべてpublicです。Pythonのように紳士協定で済ますなり、Perlのようにゲッタとセッタを通すように約束するなりする必要がありそうです。

プログラム開始時は、カレントロケールはbaseという空っぽのロケールに設定されています。

カレントロケール.ijs
echo coname '' NB. base と表示

cocurrent 'bar'
foo =: 42

cocurrent 'baz'
foo =: 54

cocurrent 'base'

echo foo_bar_ NB. 42 と表示される
echo foo_baz_ NB. 54 と表示される

loc =. < 'bar'
echo foo__loc NB. 42 と表示

loc =. < 'baz'
echo foo__loc NB. 54 と表示

foo_baz_ =: 'fourtytwo'
echo foo__loc NB. fourtytwo と表示

正確にいうと、foo_bar_は「barとbarのサーチパス上のどこかにあるfoo」を指します。例えばecho_bar_のように書いても、標準のzロケールにある動詞echo_z_が呼び出せます。

サーチパス

各ロケールには組み込みでサーチパスという変数が備わっています。

この変数で名前解決に使うロケールを順序づけて設定できます。これは現在のロケールで名前が見つからなかった時に使われます。サーチパスにあるロケールで同名の変数を順番に探していき、最初に見つかった物が使われます。

サーチパスはcopath 'ロケール名'で取得でき、coinsert '検索対象に加えるロケール名'で追加できます。

サーチパス.ijs
cocurrent 'foo'
hoge =: 42

cocurrent 'base'

echo copath 'base'
NB. +-+
NB. |z|  と表示
NB. +-+

echo hoge NB. エラーになる

coinsert 'foo'

echo copath 'base'
NB. +---+-+
NB. |foo|z|  と表示
NB. +---+-+

echo hoge
NB. 42 と表示

全てのロケールはzロケールをサーチパスに持ちます。つまり、zロケールに登録した動詞や名詞はどのロケールからでも明示せず参照できます。

サーチパスは再帰的に探索されないので、zロケールのサーチパスを拡張してもそのロケールが探索の対象になるわけではありません。

サーチパス.ijs
cocurrent 'foo'
hoge =: 42

cocurrent 'z'
coinsert 'foo'

cocurrent 'base'

echo copath 'foo'  NB. [z] と表示
echo copath 'z'    NB. [foo][z] と表示
echo copath 'base' NB. [z] と表示

echo hoge_foo_    NB. OK。42と表示
echo hoge_z_      NB. OK。42と表示
echo hoge         NB. エラーになる
echo hoge_base_   NB. エラーになる

オブジェクトはロケール

J言語のオブジェクト指向はロケールの仕組みをほぼそのまま使って実現されています。

  • オブジェクトはロケールです。
  • クラスはロケールです。
  • メソッドは動詞です。
  • メソッド呼び出しは、Object locative nameを使って動詞を呼び出すことです。メソッド名__オブジェクト名 のような書式になります。
  • 継承とはサーチパスに親クラスを加えることです。
  • インスタンス化とはロケールを動的に生成することです。

クラスの定義

クラスの定義はロケールの定義です。

ロケールに属す動詞がそのままメソッドとして扱われます。ロケールに属す名詞がクラス変数です。クラスメソッドとインスタンスメソッドの区別は文法上はありません(Perl5的な感じです)。

上で使っているcoclass(クラス定義)は、cocurrent(ロケール切り替え命令)の単なる別名です。ロケールは何度でも切り替えられるので、一つのファイル内で複数のクラスを定義することが出来ます。

インスタンス生成

インスタンスの生成はロケールの生成です。匿名の名前空間を実行時に作ります。

インスタンスの生成に使うconewは、次のような定義になっています。

conewの定義.ijs
conew =: 3 : 0
  c =. < y
  obj =. cocreate ''          NB. (2)
  coinsert__obj c             NB. (3)
  COCREATOR__obj =: coname '' NB. (4)
  obj
:
  w =. conew y
  create__w x                 NB. (5)
  w
)

Rubyっぽく意訳すると、こういう処理をしています。

conew意訳.rb
def conew(x=nil, y)          # (1)
  if x
    c = [y]
    obj = cocreate()         # (2)
    obj.coinsert(c)          # (3)
    obj.COCREATOR = coname() # (4)
    obj
  else
    w = conew(nil, y)
    w.create(x)              # (5)
    w
  end
end
  1. Rubyとしては変ですが、J言語の文法でoptionalなのはxの側です。x conew yの形で呼び出します。
  2. cocreateで匿名なロケールをその場で生成します。これがオブジェクトになります。
  3. サーチパスに引数y(クラス名)を追加しています。匿名ロケールは空っぽなので、このロケールでの参照は今後全てyのロケールに移譲されることになります。つまり、クラスを設定しています。
  4. COCREATORにはconewを呼び出したロケールが記憶されます。
  5. 左辺の引数(x)がある場合、conewはこれまでの処理に加えて作ったオブジェクトのcreateメソッドを呼びます。これがコンストラクタにあたります。

このように、一度匿名の空ロケールを動的に生成してからそのサーチパスにクラス役のロケールを設定するという方法をとっています。JavaScriptなどのプロトタイプ的な方法だと思います。

メソッド呼び出し

メソッドの呼び出しはロケールをまたいだ動詞の呼び出しです。

locative nameの形で動詞を参照する際、カレントロケールの切り替えが起こります。これがself的なコンテキストの切り替えにあたります。

インスタンス役のロケールは空っぽなので、method__object 'foo'のような呼び出しはすべてクラス役のインスタンスに移譲されます。このサーチパスを遡る過程ではカレントロケールは切り替わりません。つまり、動詞の定義がクラス役のロケールにあったとしても、メソッドは常にインスタンス役のロケール上で実行されます。

また、呼び出された動詞の中で=:による代入が起こっても、変数はインスタンスのロケールに登録されます。この仕組みによってインスタンス変数が実現されています。クラス側に同名の変数があったとしても上書きではなく隠蔽されます。

継承

継承とは、クラス役のロケールのサーチパス上に親クラスを加えることです。

サーチパスに登録するモジュール数に特に制限はないので、多重継承ができます。ルールとしてはサーチパスの最初の方にある変数が優先されることになります。

おなじみのダイアモンド継承を試してみます。

四角形ファミリー

ダイアモンド継承.ijs
NB. 平行四辺形
coclass 'Parallelogram'
  NB. ..の面積メソッド
  area =: verb define
    'Parallelogram#area'
  )

NB. 長方形
coclass 'Rectangle'
  NB. Parallelogramを継承
  coinsert 'Parallelogram'

  NB. 面積メソッド (Parallelogramのものを上書き)
  area =: verb define
    'Rectangle#area'
  )

NB. 菱型
coclass 'Rhombus'
  NB. Parallelogramを継承
  coinsert 'Parallelogram'

  NB. 面積メソッド (Parallelogramのものを上書き)
  area =: verb define
    'Rhombus#area'
  )

NB. 正方形
coclass 'Square'
  NB. RhombusとRectangleを両方継承
  coinsert 'Rhombus Rectangle'

  NB. 重複した面積メソッドを呼び出す
  test =: verb define
    echo area ''
  )

coclass 'base'

NB. インスタンスを作ってtestメソッドを呼び出してみる
hoge =. conew 'Square'
echo copath hoge
test__hoge ''
  • そのまま実行すると、Rhombus#areaと表示されました。
実行結果1
+------+-------+-------------+---------+-+
|Square|Rhombus|Parallelogram|Rectangle|z|
+------+-------+-------------+---------+-+
Rhombus#area
  • Rhombusareaの定義を消して実行すると、Parallelogram#areaと表示されました。
実行結果2
+------+-------+-------------+---------+-+
|Square|Rhombus|Parallelogram|Rectangle|z|
+------+-------+-------------+---------+-+
Parallelogram#area
  • Rhombus#areaを元に戻して、coinsert 'Rhombus Rectangle'coinsert 'Rectangle Rhombus'に変えると、実行結果はRectangle#areaになりました。
実行結果2
+------+---------+-------------+-------+-+
|Square|Rectangle|Parallelogram|Rhombus|z|
+------+---------+-------------+-------+-+
Rectangle#area

どうやら素朴な左からの深さ優先探索のようです。

幸い(?)この挙動は標準のcoinsertによるものなので、ユーザがカスタマイズする余地はあります。具体的に言うと、copath両項のほうの用法でサーチパスを丸ごと上書きできるので、これをラップして優先順の違うcoinsertをユーザが自作することはできます。

coclass 'Square'
  (;: 'Rhombus Rectangle Parallelogram z') copath 'Square'

とりあえず、上のようにすれば実行結果はこうなります。

実行結果2
+------+-------+---------+-------------+-+
|Square|Rhombus|Rectangle|Parallelogram|z|
+------+-------+---------+-------------+-+
Rhombus#area

特徴と落とし穴

オブジェクトまわりを触ってみた感触をメモします。

何一つオブジェクトではない

Jは「全てはオブジェクト」という標語とは正反対の立場にあります。基本的なアトムの型は以下のとおりです。

  • 文字列系: literal, symbol, unicode
  • 数値系: boolean, integer, rational, floating, extended, complex
  • ボックス(一階型): boxed ...

参照型はありません。オブジェクトやクラスは一級市民ではなく、ロケールの世界の住人です。オブジェクト指向まわりの機能は、ロケール名を記憶したボックス入りの文字列を扱います。

無保護

変数の保護は特にないようです。hoge_fuga_ =: 'foo'という具合に他ロケールに気軽に書き込めます。読み書き前に処理を割りこませたければJava式にゲッターやセッターを作るしか無いと思います。また、処理を挟んだとしてもcallerを調べる手立てがないので自前でアクセス制限を作るのは難しいかもしれません。

変数の隠蔽のための仕組みもないようです。クロージャのような変数を封入する仕組みもないのでPerl/JavaScript式の工夫もできませんでした。どなたか挑戦してくださるとうれしいです。(公式Wikiに例はあるのですが、どれもなんかズルい…)

プロトタイプベース?

J言語の公式サイトではただの一言も言及されていませんが、この言語のオブジェクトシステムは立派なプロトタイプベースです。

上でcocreateで生成されるロケールは匿名といいましたが、正確にはただの通し番号です。生成されたロケールは参照や書き換えも問題なく行えます。クラスがロケールで、インスタンスもロケールとなると、クラスとインスタンスを区別するものは名前のほか特にありません。役割分担の単なる見立てです。この状況はプロトタイプベースオブジェクト指向と言っていいと思います。

以下は昔見たamachangさんの例移植した例です。(言葉遊びですが)インスタンスをクラスとしてインスタンスを生成しています。

donpen.ijs
animal =. cocreate ''

breath__animal =: verb define
    echo 'すーはー'
)

NB. 鳥さんのプロトタイプ
bird =. conew > animal
fly__bird =: verb define
    echo 'ばたばた'
)

NB. ペンギンさんのプロトタイプ
penguin =. conew > bird
fly__penguin =: verb define
    echo '飛べない'
)

NB. ドンペン君
donpen =. conew > penguin
sing__donpen =: verb define
    echo 'どんどんどんどんきー♪ドンキーホーテー!'
)

breath__donpen '' NB. 動物だから呼吸できる
fly__donpen '' NB. ペンギンは飛べない
sing__donpen '' NB. ドンキホーテーの歌を歌う

NB. 実は鳥は歩ける
walk__bird =: verb define
    echo 'てくてく'
)

walk__donpen '' NB. 鳥なので「てくてく」歩ける

NB. でも、ペンギンは「てくてく」じゃなく「ぴょこぴょこ」だ
walk__penguin =: verb define
    echo 'ぴょこぴょこ'
)

walk__donpen '' NB. ぴょこぴょこ

exit 0
出力例
$ jconsole donpen.ijs
すーはー
飛べない
どんどんどんどんきー♪ドンキーホーテー!
てくてく
ぴょこぴょこ

conewがプロトタイプ系の言語のcloneと似た働きをしているのがわかると思います。conewの前に>がついているのはロケール名がボックスに包まれているのを開封するためです。

リフレクション

リフレクションのための動詞もある程度用意されています。

thisselfにあたるものは予約語としてはありませんが、self =. coname ''で似たような物が取得できます。

標準ライブラリnamesという標準動詞があります。これは呼び出したロケール内で定義された変数を一覧します。名詞や動詞など品詞(型)を指定して絞り込むこともできるので、rubyのmethods相当のことができます。

すでに紹介しましたが、copathで継承リストを、conameで現在のロケール(thisにあたる)を取得できます。これらは標準ライブラリの一部で、他にconamesで存在するロケールの一覧を取得できます。

superはないが落とし穴はある

継承はありますが、superらしい予約語はありません。単純に考えると super =: 0 { copath coname '' (rubyでいう self.class.ancestors[0]) のように一応自前で用意できますが、多重継承ができるので単純にはいかないと思います。

ところで、メソッド呼び出しの時カレントロケールが切り替わるというのは、継承のとき少し落とし穴になります。

super.ijs
coclass 'Parent'

  create =: verb define
     name =: y
  )

  NB. 挨拶するメソッド
  greet =: verb define
    echo 'Hi! I am ' , name
  )

NB. 子クラス
coclass 'Child'

  NB. Parentを継承
  coinsert 'Parent'

  create =: verb define
    NB. 子供なのでMcがつく
    myname =. 'Mc' , y
    NB. 親クラスのメソッドを呼び出したかった *ここが落とし穴*
    create_Parent_ myname
  )

NB. baseロケールに戻す
coclass 'base'

hoge =: 'Intosh' conew 'Child'
greet__hoge '' NB. Hi! I am McIntosh と表示

fuga =: 'Donald' conew 'Child'
greet__fuga '' NB. Hi! I am McDonald と表示

greet__hoge '' NB. Hi! I am McDonald と表示 (?)
echo name_Parent_ NB. McDonald と表示 (??)
echo name_Child_ NB. McDonald と表示 (???)

name変数がParentロケールで定義されてしまいました。create_Parent_ の呼び出しの時、インスタンスのロケールやChildロケールをすっとばしてParentロケールでcreateが実行されてしまったのです。これを回避するため、助詞 f. を間に挟むというイディオムがあります。

super.ijs
NB. これでOK。Parentロケールのcreateをインスタンスのロケールのまま呼び出せる
create_Parent_ f. myname

助詞f.を挟むことで名前解決をいったん完了してロケール参照でなくなるため、カレントロケールを切り替えずに呼び出すことができます。

利用例

上のamachangさんの例の他に、sumimさんのBankAccountの例がわりとすんなり移植できました。変数の読み書きに割り込む仕組みは見当たらなかったので、ゲッターとセッターを定義しました。

bank_account.ijs
bankAccount =. conew 'z'

dollars__bankAccount =: 200

NB. Not present in the original code. Defined a getter to make it customizable.
getDollars__bankAccount =: verb define
  dollars
)
NB. Not present in the original code. Defined a setter to make it customizable.
setDollars__bankAccount =: verb define
  dollars =: y
)
deposit__bankAccount =: monad define
  setDollars (getDollars '') + y
)
withdraw__bankAccount =: monad define
  setDollars 0 >. (getDollars '') - y
)

echo getDollars__bankAccount ''          NB. ==> 200

echo deposit__bankAccount 50             NB. ==> 250
withdraw__bankAccount 100
echo getDollars__bankAccount ''          NB. ==> 150
withdraw__bankAccount 200
echo getDollars__bankAccount ''          NB. ==> 0

account =. conew > bankAccount
deposit__account 500
echo getDollars__account ''              NB. ==> 500
echo getDollars__bankAccount ''          NB. ==> 0 # プロトタイプのスロットには影響なし。

stockAccount =. conew > bankAccount
numShares__stockAccount =: 10
pricePerShare__stockAccount =: 30
getNumShares__stockAccount =: verb define
  numShares
)
getDollars__stockAccount =: verb define
  numShares * pricePerShare
)
setDollars__stockAccount =: monad define
  numShares =: y % pricePerShare
) 
echo getDollars__stockAccount ''         NB. ==> 300
setDollars__stockAccount 150
echo getDollars__stockAccount ''         NB. ==> 150
echo getNumShares__stockAccount ''       NB. ==> 5 # 株数値が変更されている。

stock =. conew > stockAccount
setDollars__stock 600
echo getNumShares__stock ''              NB. ==> 20
deposit__stock 60
echo getDollars__stock ''                NB. ==> 660
echo getNumShares__stock ''              NB. ==> 22

exit 0

まとめ

J言語は素朴なオブジェクトシステムを備えています。以下のような機能が、単純な仕組みで実現されています。

機能 仕組み
クラス ロケール(名前空間)にまとめた関数と変数
インスタンス ロケールを実行時に作り、クラスを担当するロケールと関連付け
動的呼び出し 文字列変数で動的にロケールを指定
継承 サーチパスによる名前解決の連鎖

これらに加えて簡単なリフレクションやevalがあるので、メタなことが色々出来ます。少なくともattr_readerやバリデータなどの実装は難しくないと思います。ひょっとすると、Perlのように高度なオブジェクトシステムをこの上に構築することもできるのかもしれません。

リンク

16
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
8