天然パーマです。

RakuのType CheckとGradual Typingについて

YAPC::Kyoto のタイムテーブルが発表された。 Raku(Perl6)初心者のNativeCallを使った全文検索ライブラリー作成日記 を聞きたいな〜と思ったら、 裏がマコピーかんたん静的型付けPerlへの道のり だった。 マコピーのトークも気になる〜〜〜ってなって、とあるところでこうつぶやいた。

Rakuで型付けできるじゃんとか思いつつ「Perlをパースして型を抜き出す」これ面白そう、とかなる

でも、Raku(Perl 6)の型付けについてイマイチ理解が足りない。 さらに、Type Check=型チェックについて、どういう場合に、 どこで、どんなエラーを吐くかが分かってなかったのでインターネットアイドルさんの 力を借りつつ調べてみた。

追記

以下の点について、教えてもらってスッキリした。

  • Typingと型チェックは別の概念
    • 静的型付けや漸進的型付けであるのと、どういう風に型チェックするかどうかは別で考える
  • Rakuは静的型付けで記述でき、後述するように型チェックの機能が備わっている
    • ただし、エラーを吐くのがコンパイル時 or 実行時かは実装に関わる
    • my Int $i := 'foo'でコンパイルエラーが出ないのは、単にまだ実装しきれてない or なんらかの深い理由があってのことだろう

RakuはGradual Typingな言語

まずRakuの型付けの性質について。 Rakuは、Javaのような静的型付けの側面も持ちつつ、動的型付けの利点がある Gradual Typing=漸進的型付けを持つ(だと思う)。漸進的型付けについては以下の記事がすごく面白いです。

RakuはGradual Typingだぜ!って言ってる人は他にもいる。

型チェックの例

Perl 5は動的型付けな言語(ライブラリを使って漸進的型付けっぽくすることもできる)なので、 sub funcで定義したサブルーチンの引数にIntを期待したいけど、 それを表現することはできなくて、以下のコードはそのままfooと印字する。

use v5.10;

sub func {
    my $i = shift;
    say $i;
}
func('foo');

まぁ当たり前なんだけど、これをRakuでやろうとするとシグネチャーを付けてこうなる。

use v6;

sub func( Int $i ) {
    say $i;
}
func('foo');

実行しようとするとコンパイルエラーになる。

エラーメッセージが色付けされてていい感じ!

とはいえ、実はRakuにおける型チェックでコンパイルエラーが出るのは このケース(X::TypeCheck::Argument)だけのようだ。

X::TypeCheck::*

Rakuのコンパイルor実行時エラーの定義はRakudoの例外に関するソースを読むと結構分かる。

型チェックで例外を出すために実装されているのは、 X::TypeCheckを継承したクラスX::TypeCheck::*。 で、その中でも上の例で示したのはX::TypeCheck::Argumentで定義されている例外で、 これだけコンパイル時にエラーが出るみたい (コンパイルエラーを出すのはX::Compロールを実装しているものだと思うので、 ソースコードをみる限り、それを実装していないX::TypeCheck::Argumentでコンパイルエラーが出るのが謎)。

で、どういう場合に型チェックでどんなエラーが出るのかをいくか試してみた。 これらは

# 実行時エラーが出るコード
CATCH { default { put .^name, ': ', .Str } };

とすれば、例外の名前とメッセージをみることができる。

X::TypeCheck::Assignment

型宣言した変数に異なる型の値を代入しようとした時に発生する。

my Int $i = "foo";
CATCH { default { put .^name, ': ', .Str } };
# X::TypeCheck::Assignment: Type check failed in assignment to $i; expected Int but got Str ("foo")

X::TypeCheck::Return

サブルーチンの返り値が期待する型ではない時に発生する。

sub func( --> Int ) {
    return 'foo';
}
func();
CATCH { default { put .^name, ': ', .Str } };
# X::TypeCheck::Return: Type check failed for return value; expected Int but got Str ("foo")

X::TypeCheck::Binding

指定した型以外の値をバインドしようとした時に発生する。

my Int $i := "foo";
CATCH { default { put .^name, ': ', .Str } };
# X::TypeCheck::Binding: Type check failed in binding; expected Int but got Str ("foo")

X::TypeCheck::Binding::Parameter

クラス内メソッドの引数が期待する型と異なる場合に発生する。

class A {
    method func( Int $i) {
        say $i;
    }
}
A.new.func('foo');
CATCH { default { put .^name, ': ', .Str } };
# X::TypeCheck::Binding::Parameter: Type check failed in binding to parameter '$i'; expected Int but got Str ("foo")

X::TypeCheck::Assignment

クラス内のプロパティの値が期待する型と異なる場合に発生する。

class A {
    has Int $.i;
}
A.new( i => 'foo' );
CATCH { default { put .^name, ': ', .Str } };
# X::TypeCheck::Assignment: Type check failed in assignment to $!i; expected Int but got Str ("foo")

X::TypeCheck::Argument

サブルーチンの引数が期待する型と異なる場合に発生する。

sub func( Int $i ) {
     say $i;
}
func('foo');
CATCH { default { put .^name, ': ', .Str } };

、これだけコンパイルエラーになる。

===SORRY!=== Error while compiling type.p6
Calling func(Str) will never work with declared signature (Int $i)
at type.p6:19
------> <BOL>⏏func('foo');

ちなみに、ロール周りのエラー

ロール周りでわりとコンパイル時にエラーを吐いてくれて、イケてる。

例えば、複数のロールを実装した場合にメソッドが被ってしまった時は以下の通り。

role A {
    method func() {}
}

role B {
    method func() {}
}

class C does A does B {}

stubで定義したメソッドを実装しない場合。

role A {
    method func() { !!! }
}

class C does A {}

まとめる

以上、漸進的型付けなRakuではコンパイル時もしくは実行時に型チェックが走り、 X::TypeCheck::*で定義された例外を投げることが分かった。

他の言語の静的型付け、漸進的型付けがどうなっているのか、 Perl 5でどのように実現するのかが気になるところだが、 YAPC::Kyotoのマコピーのトークを聞けば分かるっぽい!! でもNativeCallでGroongaも面白そう!

わくわく〜〜〜。

追記

Twitterでつぶやいたら、

loading...

すごい勢いでエリザベスさんがレスしてくれた。

loading...

loading...