晴耕雨読

working in the fields on fine days and reading books on rainy days

OOP in Raku (Perl6)

Perl6 (Raku) におけるオブジェクト指向のまとめ

目次

クラスの定義

  • class というキーワードでクラスを宣言
  • クラス名.new() でインスタンスの生成
class Foo {}

my $foo = Foo.new;

フィールド、メソッド

  • フィールドは has $.フィールド名 で宣言します
    • 定義するときは $.フィールド名 とし、メソッドなどからアクセスするときは $!フィールド名 の方が良いです。 理由としては . トゥイジルは、アクセッサの定義と ! トゥイジルによるアクセッサの定義を行うため、処理が重いからです。
  • メソッドは method というキーワードで宣言します
class Point {
    has $.x;
    has $.y;

    method add($other) {
        Point.new(x => $!x + $other.x, y => $!y + $other.y);
    }
}

my $p1 = Point.new(x => 1, y => 2);
my $p2 = Point.new(x => 3, y => 4);
say $p1.add($p2);
# => Point.new(x => 4, y => 6)
  • フィールドに関して言えば、my による変数定義と同様に、型を指定することもできます
class Point {
    has Numberic $.x;
    has Numberic $.y;

    # ...
}

コンストラクタ

  • new というメソッドがコンストラクタになります
  • デフォルトでは、has で宣言したパブリックフィールドを名前付き引数とするコンストラクタが定義されています
class Point {
    has $.x;
    has $.y;

    method new(:$x, :$y) {
        self.bless(:$x, :$y);
    }
}

say Point.new(x => 10, y => 3.14); # => Point.new(x => 10, y => 3.14)
say Point.new();                   # => Point.new(x => Any, y => Any)
  • 必ずフィールドを初期化する必要があるとき
    • new でコンストラクタを作らない場合は、フィールドに is required を付けます
    • new で名前付き引数を取るコンストラクタを作る場合は、引数の末尾に ! を付けます
    • new で固定引数を取るコンストラクタを作ります
class Point {
    has $.x is required;
    has $.y is required;
}

say Point.new(x => 10, y => 3.14); # => Point.new(x => 10, y => 3.14)
say Point.new();                   # => Error!
class Point {
    has $.x;
    has $.y;

    method new(:$x!, :$y!) {
        self.bless(:$x, :$y);
    }
}
say Point.new(x => 10, y => 3.14); # => Point.new(x => 10, y => 3.14)
say Point.new();                   # => Error!
class Point {
    has $.x;
    has $.y;

    method new($x, $y) {
        self.bless(:$x, :$y);
    }
}
say Point.new(10, 3.14); # => Point.new(x => 10, y => 3.14)
say Point.new();         # => Error!

クラスメソッド、クラス変数

  • クラス変数は my キーワードを使います
  • クラスメソッドは オブジェクトのフィールドにアクセスしていないメソッドは、クラスメソッドとして呼ぶことができます
    • 任意のメソッドがクラスメソッドとして呼ばれた場合は、self.defined が false になります
    • 任意のメソッドがインスタンスメソッドとして呼ばれた場合は、self.defined が true になります
class User {
    has $.name;
    my $.counter = 0;

    method new($name) {
        User.counter++;
        self.bless(:$name);
    }

    method get_count() {
        User.counter;
    }
}

my $a = User.new('a');
my $b = User.new('b');
say User.counter;   # => 2
say User.get_count; # => 2

アクセス権

  • public
    • has $.フィールド名
    • method メソッド名
  • 制限付き
    • has $.フィールド名 is readonly
    • has $.フィールド名 is rw
  • private
    • has $!フィールド名
    • method !メソッド名
  • protected
    • submethod メソッド名

オーバーロード

同じ名前のメソッドを multi で定義することでオーバーロードすることができます。

class Sample {
    multi method foo(Int $a where { $a >= 0 }) {
        say "take $a apples";
    }

    multi method foo(Int $a) {
        say "give " ~ (-$a) ~ " apples";
    }

    multi method foo(Str $a) {
        say "Hello $a!"
    }
}

Sample.foo(3);       # => take 3 apples
Sample.foo(-2);      # => give 2 apples
Sample.foo("Alice"); # => Hello Alice!

継承、オーバーライド

  • 継承をするには class 子クラス is 親クラス と書きます
  • class 子クラス is 親クラス1 is 親クラス2 で多重継承になります
  • 親クラスのメソッドへの参照は nextsame を使います
class A {
    method foo {
        say "A";
    }
}

class B is A {
    method foo {
        say "B";
        nextsame;
    }
}

B.foo;
# => B
# => A

抽象クラス、インターフェース

ロールRoles)は、Javaでいうところの抽象クラスやインターフェースに当たるものです。 クラスは多重継承したときに同じ名前のメソッドが存在するときは C3 という メソッド解決順序(Method Resolution Order)アルゴリズムを使ってどちらの関数を呼ぶか決定しますが、 ロールを多重継承したときに同じ名前のメソッドが存在するときはエラーになります。 そのときは、子クラスの方でそのメソッドを実装(オーバーライド)しなければなりません。

  • ロールは role キーワードで宣言します
  • ロールを実装したいクラスは does キーワードを使って宣言します
role A {
    method foo {
        say "A";
    }
}

role B {
    method foo {
        say "B";
    }
}

class C does A does B {
    # この場合はfooメソッドが競合してしまうので、オーバーライドしないとエラーになる
    method foo {
        say "A";
        say "B";
    }
}

C.foo;
# => A
# => B

演算子オーバーライド

演算子には prefix, postfix, infix, circumfix, postcircumfix の5種類があります。

種類 定義
prefix prefix:<!> !3
postfix postfix:<!> 3!
infix infix:<+> 1 + 2
circumfix circumfix:<[ ]> [1]
postcircumfix postcircumfix:<[ ]> a[3]
class Point {
    has $.x;
    has $.y;

    method add($other) {
        Point.new(x => $!x + $other.x, y => $!y + $other.y);
    }
}


# prefix: -
# Retruns negated vector
# ex) -a
multi sub prefix:<->(Point $p) {
    Point.new(x => -$p.x, y => -$p.y);
}
say -Point.new(x => 1, y => 2);
# => Point.new(x => -1, y => -2)


# postfix: ~
# Retruns transposed vector
# ex) a~
multi sub postfix:<~>(Point $p) {
    Point.new(x => $p.y, y => $p.x);
}
say Point.new(x => 1, y => 2)~;
# => Point.new(x => 2, y => 1)


# infix: +
# Add two vectors
# ex) a + b
multi sub infix:<+>(Point $lhs, Point $rhs) {
    $lhs.add($rhs);
}
say Point.new(x => 1, y => 2) + Point.new(x => 3, y => 4);
# => Point.new(x => 4, y => 6)


# circumfix: []
# Computes distance from origin
# ex) [a]
multi sub circumfix:<[ ]>(Point $p) {
    sqrt($p.x ** 2 + $p.y ** 2);
}
say [Point.new(x => 3, y => 4)];
# => 5


# postcircumfix: []
# Add two vectors
# ex) point[1, 2]
multi sub postcircumfix:<[ ]>(Point $p, @index where { @index.elems == 2 }) {
    Point.new(x => $p.x + @index[0], y => $p.y + @index[1]);
}
say Point.new(x => 1, y => 2)[3, 4];
# => Point.new(x => 4, y => 6)

演算子の定義

演算子は自分で定義することもできます。 次の例は、n日後の日にちを取得するための演算子 days の定義方法です。

sub infix:<days>(Int $day, %args) {
    if %args<ago>:exists {
        return Date.new(DateTime.now).later(days => $day);
    }
}

say 3 days :ago;