プログラミング言語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.
で始まります)
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
)
作ったクラスは、次のように利用します。
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 ''
で取得できます。
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
という空っぽのロケールに設定されています。
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 '検索対象に加えるロケール名'
で追加できます。
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
ロケールのサーチパスを拡張してもそのロケールが探索の対象になるわけではありません。
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 =: 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っぽく意訳すると、こういう処理をしています。
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
- Rubyとしては変ですが、J言語の文法でoptionalなのはxの側です。
x conew y
の形で呼び出します。 -
cocreate
で匿名なロケールをその場で生成します。これがオブジェクトになります。 - サーチパスに引数
y
(クラス名)を追加しています。匿名ロケールは空っぽなので、このロケールでの参照は今後全てyのロケールに移譲されることになります。つまり、クラスを設定しています。 -
COCREATOR
にはconew
を呼び出したロケールが記憶されます。 - 左辺の引数(
x
)がある場合、conew
はこれまでの処理に加えて作ったオブジェクトのcreate
メソッドを呼びます。これがコンストラクタにあたります。
このように、一度匿名の空ロケールを動的に生成してからそのサーチパスにクラス役のロケールを設定するという方法をとっています。JavaScriptなどのプロトタイプ的な方法だと思います。
メソッド呼び出し
メソッドの呼び出しはロケールをまたいだ動詞の呼び出しです。
locative nameの形で動詞を参照する際、カレントロケールの切り替えが起こります。これがself
的なコンテキストの切り替えにあたります。
インスタンス役のロケールは空っぽなので、method__object 'foo'
のような呼び出しはすべてクラス役のインスタンスに移譲されます。このサーチパスを遡る過程ではカレントロケールは切り替わりません。つまり、動詞の定義がクラス役のロケールにあったとしても、メソッドは常にインスタンス役のロケール上で実行されます。
また、呼び出された動詞の中で=:
による代入が起こっても、変数はインスタンスのロケールに登録されます。この仕組みによってインスタンス変数が実現されています。クラス側に同名の変数があったとしても上書きではなく隠蔽されます。
継承
継承とは、クラス役のロケールのサーチパス上に親クラスを加えることです。
サーチパスに登録するモジュール数に特に制限はないので、多重継承ができます。ルールとしてはサーチパスの最初の方にある変数が優先されることになります。
おなじみのダイアモンド継承を試してみます。
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
と表示されました。
+------+-------+-------------+---------+-+
|Square|Rhombus|Parallelogram|Rectangle|z|
+------+-------+-------------+---------+-+
Rhombus#area
-
Rhombus
のarea
の定義を消して実行すると、Parallelogram#area
と表示されました。
+------+-------+-------------+---------+-+
|Square|Rhombus|Parallelogram|Rectangle|z|
+------+-------+-------------+---------+-+
Parallelogram#area
-
Rhombus#area
を元に戻して、coinsert 'Rhombus Rectangle'
をcoinsert 'Rectangle Rhombus'
に変えると、実行結果はRectangle#area
になりました。
+------+---------+-------------+-------+-+
|Square|Rectangle|Parallelogram|Rhombus|z|
+------+---------+-------------+-------+-+
Rectangle#area
どうやら素朴な左からの深さ優先探索のようです。
幸い(?)この挙動は標準のcoinsert
によるものなので、ユーザがカスタマイズする余地はあります。具体的に言うと、copath
の両項のほうの用法でサーチパスを丸ごと上書きできるので、これをラップして優先順の違うcoinsert
をユーザが自作することはできます。
coclass 'Square'
(;: 'Rhombus Rectangle Parallelogram z') copath 'Square'
とりあえず、上のようにすれば実行結果はこうなります。
+------+-------+---------+-------------+-+
|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さんの例を移植した例です。(言葉遊びですが)インスタンスをクラスとしてインスタンスを生成しています。
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
の前に>
がついているのはロケール名がボックスに包まれているのを開封するためです。
リフレクション
リフレクションのための動詞もある程度用意されています。
this
やself
にあたるものは予約語としてはありませんが、self =. coname ''
で似たような物が取得できます。
標準ライブラリにnames
という標準動詞があります。これは呼び出したロケール内で定義された変数を一覧します。名詞や動詞など品詞(型)を指定して絞り込むこともできるので、rubyのmethods
相当のことができます。
すでに紹介しましたが、copath
で継承リストを、coname
で現在のロケール(this
にあたる)を取得できます。これらは標準ライブラリの一部で、他にconames
で存在するロケールの一覧を取得できます。
superはないが落とし穴はある
継承はありますが、super
らしい予約語はありません。単純に考えると super =: 0 { copath coname ''
(rubyでいう self.class.ancestors[0]
) のように一応自前で用意できますが、多重継承ができるので単純にはいかないと思います。
ところで、メソッド呼び出しの時カレントロケールが切り替わるというのは、継承のとき少し落とし穴になります。
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.
を間に挟むというイディオムがあります。
NB. これでOK。Parentロケールのcreateをインスタンスのロケールのまま呼び出せる
create_Parent_ f. myname
助詞f.
を挟むことで名前解決をいったん完了してロケール参照でなくなるため、カレントロケールを切り替えずに呼び出すことができます。
利用例
上のamachangさんの例の他に、sumimさんのBankAccountの例がわりとすんなり移植できました。変数の読み書きに割り込む仕組みは見当たらなかったので、ゲッターとセッターを定義しました。
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のように高度なオブジェクトシステムをこの上に構築することもできるのかもしれません。
リンク
- J言語 : 処理系の配布元
- Chapter 25: Object-Oriented Programming - Learning J : 公式チュートリアルのOOPについての章
- Locale - J Primer : ロケールのチュートリアル
- Locatives - J Dictionary : Locative(ロケール関係の文法)について
- Locales - J Wiki/Vocabulary : 名前解決について
- colib.ijs - J Help/Users Manual : ロケール操作の標準ライブラリ