クラス
Wrenのすべての値はオブジェクトで、すべてのオブジェクトはクラスのインスタンスです。trueとfalseも、完全な機能を持つオブジェクトで、Boolクラスのインスタンスです。
クラスは、オブジェクトの振る舞いと状態を定義します。振る舞いは、クラスに置くメソッドで定義します。同じクラスのオブジェクトは、すべて同じメソッドに対応します。状態はフィールドで定義し、その値は各インスタンスに保存します。
クラスの定義 #
予想どおり、クラスはclassキーワードで作成します。
class Unicorn {}
これで、メソッドもフィールドも持たない、Unicornというクラスを作成します。
メソッド #
ユニコーンが何かできるようにするには、メソッドを与える必要があります。
class Unicorn {
prance() {
System.print("The unicorn prances in a fancy manner!")
}
}
これで、引数を取らないprance()メソッドを定義します。パラメーターを追加するには、丸括弧の中にその名前を書きます。
class Unicorn {
prance(where, when) {
System.print("The unicorn prances in %(where) at %(when).")
}
}
パラメーターの個数はメソッドのシグネチャの一部なので、一つのクラスに同じ名前のメソッドを複数定義できます。
class Unicorn {
prance() {
System.print("The unicorn prances in a fancy manner!")
}
prance(where) {
System.print("The unicorn prances in %(where).")
}
prance(where, when) {
System.print("The unicorn prances in %(where) at %(when).")
}
}
同じ概念的な操作を、異なる引数の組で使えるようにするのは、自然なことです。ほかの言語では、操作のために一つのメソッドを定義し、省略可能な引数が渡されていないかを調べる必要があります。Wrenでは、これらを別々のメソッドとして、それぞれ実装します。
パラメーターリストを持つ名前付きメソッドに加え、Wrenには、メソッドのためのさまざまな構文があります。自分のクラスでも、そのすべてを定義できます。
ゲッター #
ゲッターでは、パラメーターリストと丸括弧を省きます。
class Unicorn {
// Unicorns are always fancy.
isFancy { true }
}
セッター #
セッターでは、名前の後に=を置き、その後に丸括弧で囲んだ一つのパラメーターを書きます。
class Unicorn {
rider=(value) {
System.print("I am being ridden by %(value).")
}
}
慣例として、このパラメーターには通常valueという名前を付けますが、好きな名前を付けてかまいません。
演算子 #
前置演算子には、ゲッターと同様、パラメーターリストがありません。
class Unicorn {
- {
System.print("Negating a unicorn is weird.")
}
}
中置演算子には、セッターと同様、右のオペランドのための、丸括弧で囲んだ一つのパラメーターがあります。
class Unicorn {
-(other) {
System.print("Subtracting %(other) from a unicorn is weird.")
}
}
添字演算子では、角括弧の中にパラメーターを書きます。複数のパラメーターも使えます。
class Unicorn {
[index] {
System.print("Unicorns are not lists!")
}
[x, y] {
System.print("Unicorns are not matrices either!")
}
}
名前付きメソッドとは異なり、パラメーターリストが空の添字演算子は定義できません。
名前が示すとおり、添字セッターは、添字演算子とセッターを組み合わせた形になります。
class Unicorn {
[index]=(value) {
System.print("You can't stuff %(value) into me at %(index)!")
}
}
メソッドのスコープ #
ここまで、スコープという語は、変数についてだけ使ってきました。Cのような手続き型言語や、Schemeのような関数型言語では、スコープはこの一種類だけです。しかし、Wrenのようなオブジェクト指向言語には、もう一つのオブジェクトスコープがあります。これは、そのオブジェクトで利用できるメソッドを含みます。次のように書くと、
unicorn.isFancy
「オブジェクトunicornのスコープで、メソッドisFancyを探す」と指示しています。この場合、探したいものが変数isFancyではなく、メソッドisFancyであることは明示されています。それを表すのが.であり、ピリオドの左側のオブジェクトが、メソッドを探す対象です。
this #
メソッドの本体の中では、もう少し複雑になります。あるオブジェクトのメソッドが呼び出され、その本体を実行しているときには、そのオブジェクト自身へアクセスしたいことがよくあります。thisを使うと、それができます。
class Unicorn {
name { "Francis" }
printName() {
System.print(this.name) //> Francis
}
}
thisキーワードは、変数のように働きますが、特別な振る舞いがあります。常に、現在実行しているメソッドを持つインスタンスを参照します。これにより、「自分自身」のメソッドを呼び出せます。
メソッドの外でthisを参照すると、エラーになります。しかし、メソッドの中で宣言した関数の中なら、問題なく使えます。その場合も、thisは、呼び出されたメソッドを持つインスタンスを参照します。
class Unicorn {
name { "Francis" }
printNameThrice() {
(1..3).each {
// Use "this" inside the function passed to each().
System.print(this.name) //> Francis
} //> Francis
} //> Francis
}
これは、メソッドの中でコールバックを作ると、thisを「忘れる」場合があるLuaやJavaScriptとは異なります。Wrenでは、この場面で意図どおり、元のオブジェクトへの参照を保持します。
(専門的に言うと、関数のクロージャーにはthisも含まれます。Wrenがこれをできるのは、メソッドと関数を区別しているためです。)
暗黙のthis #
自分自身のメソッドを呼ぶたびにthis.を使うことはできますが、面倒で冗長です。そのため、一部の言語では必須にしていません。明示的なレシーバーなしでメソッド(またはゲッターやセッター)を呼び出すと、「自己への送信」ができます。
class Unicorn {
name { "Francis" }
printName() {
System.print(name) //> Francis
}
}
クラスの外にも同じ名前の変数があると、このようなコードは複雑になります。次の例を考えてください。
var name = "variable"
class Unicorn {
name { "Francis" }
printName() {
System.print(name) // ???
}
}
printName()は「variable」と「Francis」のどちらを表示すべきでしょうか? メソッドの本体は、二つの世界にまたがっています。プログラム内で定義した位置のレキシカルスコープに囲まれていますが、同時に、thisのメソッドからなるオブジェクトスコープも持ちます。
どちらのスコープが優先されるのでしょうか? 各言語は、これをどう扱うか決める必要があり、驚くほど多くの方法があります。Wrenは、メソッド内の名前を次の手順で解決します。
- メソッド内にその名前のローカル変数があれば、それを優先する。
- そうでなく、名前が小文字で始まるなら、
thisのメソッドとして扱う。 - それ以外なら、周囲のスコープでその名前の変数を探す。
したがって、上の例では二番目の場合に該当し、「Francis」を表示します。名前の最初の文字の大小によって、自己への送信と外側の変数を区別するのは、奇妙に思えるかもしれませんが、意外にうまく機能します。Wrenのメソッド名は小文字で、クラス名は大文字で始まります。
メソッドの中からクラスの外の名前へアクセスしたいとき、その名前はたいてい、別のクラスの名前です。この規則によって、それが機能します。
次は、三つの場合をすべて示す例です。
var shadowed = "surrounding"
var lowercase = "surrounding"
var Capitalized = "surrounding"
class Scope {
shadowed { "object" }
lowercase { "object" }
Capitalized { "object" }
test() {
var shadowed = "local"
System.print(shadowed) //> local
System.print(lowercase) //> object
System.print(Capitalized) //> surrounding
}
}
少し変わった規則ですが、Rubyも、おおむね同じように動作します。
コンストラクター #
オブジェクトの種類を定義し、そのメソッドを宣言する方法を見てきました。ユニコーンは跳ね回れますが、そうするためのユニコーン自体を、まだ持っていません。クラスのインスタンスを作るには、コンストラクターが必要です。次のように定義します。
class Unicorn {
construct new(name, color) {
System.print("My name is " + name + " and I am " + color + ".")
}
}
constructキーワードは、コンストラクターを定義していることを表し、newは、その名前です。Wrenでは、すべてのコンストラクターに名前があります。「new」はWrenで特別な語ではなく、よく使われるコンストラクター名にすぎません。
ユニコーンを作るには、クラス自身のコンストラクターメソッドを呼び出します。
var fred = Unicorn.new("Fred", "palomino")
コンストラクターに名前を付けると、複数のコンストラクターを持て、それぞれがどのようにインスタンスを作るか明確にできるので、便利です。
class Unicorn {
construct brown(name) {
System.print("My name is " + name + " and I am brown.")
}
}
var dave = Unicorn.brown("Dave")
ほかの一部の言語とは異なり、Wrenは既定のコンストラクターを提供しないため、自分で宣言する必要があります。これは、構築することを想定していないクラスもあるので、役立ちます。ほかのクラスが継承するメソッドだけを含む抽象基底クラスなら、コンストラクターは不要で、持つこともありません。
ほかのメソッドと同様、コンストラクターにも当然、引数を渡せ、引数の個数によるオーバーロードもできます。コンストラクターは、空でもよい引数リストを持つ名前付きメソッドでなければなりません。演算子、ゲッター、セッターをコンストラクターにはできません。
コンストラクターは、returnを明示的に使わなくても、作成しているクラスのインスタンスを返します。コンストラクター内でreturnを使うことはできますが、returnの後に式を書くとエラーになります。この規則はreturn thisにも当てはまります。コンストラクター内では、returnがそれを暗黙に扱うため、returnだけで十分です。
return //> valid, returns 'this'
return variable //> invalid
return null //> invalid
return this //> also invalid
実際には、コンストラクターは二つのメソッドの組です。クラス側に、次のメソッドができます。
Unicorn.brown("Dave")
これは新しいインスタンスを作り、続いて、そのインスタンスの初期化メソッドを呼び出します。定義したコンストラクター本体を実行するのは、こちらです。
この区別は重要です。これにより、コンストラクターの本体の中でthisへアクセスし、フィールドへ代入し、スーパークラスのコンストラクターを呼び出すことなどができます。
フィールド #
インスタンスに保存する状態は、すべてフィールドに保存します。各フィールドの名前は、アンダースコアで始まります。
class Rectangle {
area { _width * _height }
// Other stuff...
}
ここでは、areaゲッター内の_widthと_heightは、長方形のインスタンスのフィールドを参照します。ほかの言語のthis.widthやthis.heightのようなものと考えられます。
フィールド名が現れると、Wrenは、最も内側の、その位置を囲むクラスを探し、そのクラスのインスタンスでフィールドを探します。フィールド名は、インスタンスメソッドの外では使えません。メソッド内の関数の中では使えます。Wrenは、入れ子になった関数の外側を、その位置を囲むメソッドが見つかるまで探します。
変数とは異なり、フィールドは、代入するだけで暗黙に宣言されます。初期化する前にフィールドへアクセスすると、その値はnullです。
カプセル化 #
Wrenでは、すべてのフィールドは非公開です。オブジェクトのフィールドへ直接アクセスできるのは、そのオブジェクトのクラスで定義したメソッドの内部だけです。
つまり、オブジェクトのプロパティーを外から見えるようにするには、それを公開するゲッターを定義する必要があります。
class Rectangle {
width { _width }
height { _height }
// ...
}
外側のコードからフィールドを変更できるようにするには、アクセス用のセッターを提供する必要があります。
class Rectangle {
width=(value) { _width = value }
height=(value) { _height = value }
}
なじみのある動作とは違うかもしれませんので、重要な二つの事実を挙げます。
- 基底クラスのフィールドにはアクセスできません。
- 自分と同じクラスの別インスタンスのフィールドにはアクセスできません。
次は、コードによる例です。
class Shape {
construct new() {
_shape = "none"
}
}
class Rectangle is Shape {
construct new() {
//This will print null!
//_shape from the parent class is private,
//we are reading `_shape` from `this`,
//which has not been set, so returns null.
System.print("I am a %(_shape)")
//a local variable, all variables are private
_width = 10
var other = Rectangle.new()
//other._width is not accessible from here,
//even though we are also a rectangle. The field
//is private, and other._width is invalid syntax!
}
}
...
過去40年のソフトウェア工学で学んだことの一つは、状態をカプセル化するとコードを保守しやすくなる傾向があることです。そのため、Wrenでは、既定でオブジェクトの状態をかなり厳密に閉じ込めます。オブジェクトのフィールドの大半について、ゲッターやセッターを定義しなければならない、あるいは定義すべきだと考える必要はありません。
メタクラスと静的メンバー #
TODO
静的フィールド #
アンダースコア二つで始まる名前は、静的フィールドです。フィールドと同様に働きますが、データはインスタンスではなく、クラス自身に保存します。インスタンスメソッドと静的メソッドの両方で使えます。
class Foo {
construct new() {}
static setFromStatic(a) { __a = a }
setFromInstance(a) { __a = a }
static printFromStatic() {
System.print(__a)
}
printFromInstance() {
System.print(__a)
}
}
インスタンスフィールドと同様、静的フィールドの初期値はnullです。
Foo.printFromStatic() //> null
静的メソッドから使えます。
Foo.setFromStatic("first")
Foo.printFromStatic() //> first
インスタンスメソッドからも使えます。その場合も、クラスのすべてのインスタンスで共有する静的フィールドは一つだけです。
var foo1 = Foo.new()
var foo2 = Foo.new()
foo1.setFromInstance("second")
foo2.printFromInstance() //> second
継承 #
クラスは、「親」またはスーパークラスから継承できます。あるクラスのオブジェクトでメソッドを呼び出し、それが見つからない場合は、スーパークラスの連鎖を上へたどって探します。
既定では、新しいクラスはObjectを継承します。Objectは、ほかのすべてのクラスが最終的に派生するスーパークラスです。クラスの宣言時にisを使うと、別の親クラスを指定できます。
class Pegasus is Unicorn {}
これで、Unicornを継承する、新しいPegasusクラスを宣言します。
組み込み型(Bool、Num、String、Range、List)を継承するクラスを作るべきではない点に注意してください。組み込み型は、内部のビット表現が非常に特定の形であることを前提とします。派生型で、継承した組み込みメソッドを呼び出すと、ひどく混乱してしまいます。
メタクラスの階層は、通常のクラス階層と同じ継承関係にはなりません。したがって、PegasusがUnicornを継承していても、PegasusのメタクラスはUnicornのメタクラスを継承しません。より平易に言うと、静的メソッドは継承しません。
class Unicorn {
// Unicorns cannot fly. :(
static canFly { false }
}
class Pegasus is Unicorn {}
Pegasus.canFly //! Static methods are not inherited.
これは、コンストラクターも継承しないという意味です。
class Unicorn {
construct new(name) {
System.print("My name is " + name + ".")
}
}
class Pegasus is Unicorn {}
Pegasus.new("Fred") //! Pegasus does not define new().
各クラスは、基底クラスから独立して、自分をどのように構築できるか制御します。ただし、コンストラクターの初期化メソッドは、新しいオブジェクトのインスタンスメソッドなので、継承します。
つまり、コンストラクター内でsuperの呼び出しができます。
class Unicorn {
construct new(name) {
System.print("My name is " + name + ".")
}
}
class Pegasus is Unicorn {
construct new(name) {
super(name)
}
}
Pegasus.new("Fred") //> My name is Fred
Super #
TODO:ページによりよく組み込む。上でsuperに言及する前に、これを説明するべき。
自分自身のメソッドを呼び出しつつ、スーパークラスの一つで定義したメソッドを使いたい場合があります。通常は、オーバーライドしたメソッドから、オーバーライド元のメソッドにアクセスしたいときに行います。
そのためには、メソッド呼び出しのレシーバーとして、特別なsuperキーワードを使えます。
class Base {
method() {
System.print("base method")
}
}
class Derived is Base {
method() {
super.method() //> base method
}
}
コンストラクター内では、メソッド名なしのsuperを使って、基底クラスのコンストラクターを呼び出すこともできます。
class Base {
construct new(arg) {
System.print("base got " + arg)
}
}
class Derived is Base {
construct new() {
super("value") //> base got value
}
}
属性 #
実験段階:小さな変更が加わる可能性があります
クラスや、クラス内のメソッドには、「メタ属性」を付けられます。
次のように書きます。
#hidden = true
class Example {}
これらの属性はメタデータです。クラスについての任意の追加情報を注記し、保存する方法を提供し、必要に応じて実行時にアクセスできます。この情報は外部ツールでも利用でき、コードからツールへ追加のヒントや情報を提供できます。
この機能は導入されたばかりなので、注意してください。
現時点では、組み込みの意味を持つ属性はありません。属性は、ユーザーが定義するメタデータです。ただし、慣例や、Wren自身による利用によって明確に定義される属性が出てくる可能性があり、この状態が続くとは限りません。
属性は、クラスまたはメソッドの定義の前に置き、#のハッシュ記号を使います。
次の形式を使えます。
#keyだけ#key = value#group(with, multiple = true, keys = "value")
属性のキーに使えるのは、Nameだけです。これは、メソッド名、クラス名、変数名と同じ種類の名前で、Wrenの識別子の規則に従う識別子です。名前は、実行時にはStringの値になります。
属性の値には、Name, String, Bool, Numというリテラル値のいずれかを使えます。値には式を含められず、値だけを指定します。コンパイル時の評価はありません。
グループは複数行にまたがれます。メソッドには独自の属性があり、キーの重複も有効です。
#key
#key = value
#group(
multiple,
lines = true,
lines = 0
)
class Example {
#test(skip = true, iterations = 32)
doStuff() {}
}
実行時に属性へアクセスする #
既定では、属性はコンパイル時に除外して無視します。
属性を実行時に見えるようにするには、感嘆符を使って、実行時アクセスの対象として指定します。
#doc = "not runtime data"
#!runtimeAccess = true
#!maxIterations = 16
実行時の属性は、クラスに保存します。YourClass.attributesでアクセスできます。クラスに属性がない場合、または実行時アクセスの指定がない場合、そのクラスのattributesフィールドはnullになります。
クラスにクラス属性またはメソッド属性があれば、これは二つのゲッターを持つオブジェクトになります。
- クラス属性のための
YourClass.attributes.self - メソッド属性のための
YourClass.attributes.methods
属性は、通常のWren Map内に、グループごとに保存します。グループに属さないキーでは、グループのキーとしてnullを使います。
キーの重複が許されるため、複数の値を保存する必要があり、値はリストに保存します。保存順は、定義した順です。
メソッド属性は、メソッドのシグネチャをキーとするマップに保存します。各メソッドには、上の構造に合う独自の属性があります。必要に応じて、メソッドのシグネチャの前にstaticまたはforeign staticを付けます。
どのような形になるか、見てみましょう。
// Example.attributes.self =
// {
// null: { "key":[null] },
// group: { "key":[value, 32, false] }
// }
#!key
#ignored //compiled out
#!group(key=value, key=32, key=false)
class Example {
#!getter
getter {}
// { regular(_,_): { regular:[null] } }
#!regular
regular(arg0, arg1) {}
// { static other(): { isStatic:[true] } }
#!isStatic = true
static other()
// { foreign static example(): { isForeignStatic:[32] } }
#!isForeignStatic=32
foreign static example()
}
並行処理 →← 関数