ntaoo blog

主にDart, Flutter, AngularDartについて書いていきます。ときどき長文。日本語のみ。Twitter: @ntaoo

DartのMixinについての解説 (Dart 2.1対応)

Mixinに馴染みがない人が多いようなので、解説する。

Mixinとは

Mixinとは、fieldやproperty, slotなどと呼ばれている状態 (state)、およびmethodなどと呼ばれている振る舞い (behavior) の集合を定義し、それをclassに適用して拡張するもの。関数スタイルのプログラミングにおける関数の合成のように、classを合成する。

主に、Classベースのオブジェクト指向言語において、(単一)継承の階層という制約では表現することが難しいデザインには、Mixinを適用する。

主な言語における採用事例

言語レベルで採用していたり(Dart, Scala, Ruby等)、あるいはデザインパターンのひとつとして紹介されていたり(JavaScript, Swift等)する。

Mixinの起源

1990年にはMixinについての論文が存在する。 OOPSLA '90, Mixin based inheritance (pdf)

ちなみに上記論文はDart ver1の言語デザイナーによるもの。 あるLisp方言で概念が紹介され、Strongtalkで初めて実装された。

DartにおけるMixin

Dartは言語レベルでMixinを採用しており、Collectionライブラリ、Flutter framework, AngularDartをはじめ、あらゆるライブラリ、フレームワークで利用されている。

Dartでは、すべてのclassは暗黙的に自身のmixinを宣言しており、mixinとして振る舞うことができる。ただし、このシンプルな仕様には残念ながらいくつかの問題があるとみなされ、Dart 2.1において、mixin宣言専用の構文を用意するなどして仕様が改訂された。

入門

Mixinの宣言

mixin 識別子による宣言

Dart 2.1から、Mixin専用の構文が追加された。これからmixinを定義するコードを書くならば、おおむねこの構文で書けば良い。

mixin M {}

class宣言と同様に、mixin宣言のbodyにmemberを書いていく。( memberとは、constructor、 field、およびmethodを指す。ただし、mixin宣言のbodyにはconstructorを書けない制約がある。将来はこの制約は緩和されるかもしれない。 )

mixin M {
  int anIntField = 42;
  String aStringMethod() => "M's method";
}

class 識別子による暗黙的な宣言

従来の、class宣言での暗黙的なmixinの生成も可能。ただし、残念ながらこの仕様は、mixinの仕様書では、将来は非推奨 (@deprecated) とし、廃止するだろうと記されている。

abstract class M {
  int anIntField = 42;
  String aStringMethod() => "M's method";
}

Mixinの適用

with句でsuper classにmixinを適用する。

class C extends Object with M {}
final c = C();
expect(c is M, true);
expect(c is C, true);

expect(c.anIntField, 42);
expect(c.aStringMethod(), "M's method");

なお、Dart 2.1からは、構文上、extends Objectを省略できるようになった。

class C with M {}

また、これは意味上は、

class C extends M {}

と書いても同じである。これはDart 1では頻出のイディオムだったが、class C with M {}と書けるようになったので、今はそう書けば良いだろう。

Mixinの詳細の解説

前述の通り、Mixinは、fieldおよびmethodの集合を定義し、それをclassに適用して拡張するもの。関数スタイルのプログラミングにおける関数の合成のように、classを合成する。

Mixinは、「super classに」適用され、新しい「匿名の」classを生み出す。これを「Mixin Application」と呼ぶ。

MixinのInterface

このMixin Applicationがされた「匿名の」classを継承したclassは、その結果として、自身のmemberに加えてMixin Applicationのmemberのinterfaceを実装している。

class C extends S with M1, M2, M3 {}

上記の例においては、class Cは、super class Sに、mixinであるM1, M2, M3を適用したmixin applicationであるclassを継承している。

final c = C();
expect(c is M1, true);
expect(c is M2, true);
expect(c is M3, true);
expect(c is S, true);
expect(c is C, true);

isは、objectのclassがinterfaceを(直接的、または間接的に)実装する宣言をしているかを検査する。Dartでは、すべてのclassおよびmixinは、暗黙的に自身のinterfaceを宣言している。

Dartのユニークな特徴として、is検査は、objectのclassではなく、あくまでinterfaceを対象にして検査している。たとえば、ある2つのobjectがあり、それらの内部の実装が異なっても、interfaceが同じならば、両者は区別されない (modulo reflection)。

(* Interfaceは、objectのアクセス可能なmethodのシグネチャの集合である。fieldにはカプセル化の規則に応じて暗黙的に対応するgetter method、setter methodが宣言され、そのmethodを通じてfieldにアクセスする。)

Mixinのmember

Mixinのmemberとは、fieldおよびmethodを指す。 classと同様に、mixinのbodyにmemberを書いていく。

mixin M1 {
  int aM1Field = 42;
  String aM1Method() => "M1 method";
}
mixin M2 {
  int aM2Field = 43;
  String aM2Method() => "M2 method";
}
mixin M3 {
  int aM3Field = 44;
  String aM3Method() => "M3 method";
}
class S {
  int aSField = 45;
  String aSMethod() => "S method";
}

class C extends S with M1, M2, M3 {
  int aCField = 46;
  String aCMethod() => "C method";
}

Mixin Applicationを継承したCには、自身のmemberに加えてMixin Applicationのmemberのinterfaceが実装されている。

expect(c.aM1Field, 42);
expect(c.aM2Field, 43);
expect(c.aM3Field, 44);
expect(c.aSField, 45);
expect(c.aCField, 46);
expect(c.aM1Method(), "M1 method");
expect(c.aM2Method(), "M2 method");
expect(c.aM3Method(), "M3 method");
expect(c.aSMethod(), "S method");
expect(c.aCMethod(), "C method");

Memberの探索順序

複数のmixinに同名のmemberがある場合、どのmixinのものを優先するかには明快な規則がある。

  1. まずは、mixin applicationを継承したclassが探索される。
  2. 次に、後から追加したmixinのmemberが優先して探索される。
  3. 最後に、class階層に沿って探索される。

Linerlization (線形化)

たとえば、以下のコードでは、aMethodがそれぞれのbodyに定義されている。

mixin M1 {
  String aMethod() => "M1 method";
}
mixin M2 {
  String aMethod() => "M2 method";
}
mixin M3 {
  String aMethod() => "M3 method";
}
class S {
  String aMethod() => "SS method";
}
class SS extends S {
  String aMethod() => "SS method";
}

そして、Mixin Applicationを継承したCを以下に定義する。

class C extends SS with M1, M2, M3 {
  String aMethod() => "C method";
}

この場合、methodの探索の順序は、C -> M3 -> M2 -> M1 -> SS -> S -> Objectである。

つまり、

final c = C();
expect(c.aMethod(), "C method");

となる。

もし、CaMethod()が定義されていなければ、expect(c.aMethod(), "M3 method");となる。M3に定義されていなければ、expect(c.aMethod(), "M2 method");と、後から追加したmixinのmemberが優先して探索される。その後、SS, S, Objectと、class階層に沿って探索される。

なお、private memberについてもpublic memberと同様にmixinの対象となる。ただし、libraryを超えてmixinする場合は、private memeberにはアクセスできない。

( 注:Dartカプセル化の単位はlibraryである。Private memberは名前にunderscore prefixがついたものである。アクセス修飾子は存在しない。デフォルトでpublic memberとなる。https://www.dartlang.org/guides/language/language-tour#libraries-and-visibility

Mixin Applicationのclassの名付け

Mixin Applicationのclassを、匿名でなく名付けしたい場合は、以下の構文で可能となる。

class CSuper = S with M1, M2, M3;

上記では、CSuperと名付けた。(body ({}) は不要。)

ここで、S, M1, M2, M3の実装は、Linerlizationのセクションのサンプルコードを再利用することとする。

final cSuper = CSuper();
expect(cSuper.aMethod(), "M3 method");

CSuperは、SM1, M2, M3を適用したmixin applicationなので、最後に適用されたM3のmemberが最も優先して探索される。 (規則通り、Sのmemberの優先度は最も低いことに注意。)

さらに、Mixin Applicationのclassに、さらにmixinを追加することも可能。

mixin M4 {
  String aMethod() => "M4 method";
}

class CSuper2 = CSuper with M4;

CSuper2は、CSuperM4を適用したMixin Application classとなる。

final cSuper2 = CSuper2();
expect(cSuper2.aMethod(), "M4 method");

最後に適用されたM4のmemberが最も優先して探索される。

on句とsuper

mixin宣言におけるon句は、super classの制約を宣言する。

  • on句で宣言されたinterfaceを実装したsuper classでのみこのmixinを適用可能となる。
  • mixinのinstance member methodにおいて、super classのsuper invocationを可能にする。(super.foo()).

この制約を満たさないコードはコンパイル時エラーとなる。また、Static analyzerからエラーが報告される。

mixin M on S {
  String aMethod() {
    print(super.aMethod());
    return 'a M Method.';
  }
}

class S {
  String aMethod() => 'a S Method.';
}

class C extends S with M {}
final c = C();
print(c.aMethod());
// a S Method.
// a M Method.

on句は省略可能であり、on Objectと同じ意味となる。

implements句

class宣言と同様に、mixin宣言でもimplements句でinterfaceを宣言し、静的な型検査およびisによる実行時型検査を利用できる。

Generics

classと同様に、mixinにも型パラメーターを追加できる。

現時点での制約

  • mixinにはconstructorを定義できない。この制約は、仕様書によれば将来は緩和されるかもしれない。
  • mixinは別のmixinをextendすることができない。この制約は、仕様書によれば将来は緩和されるかもしれない。

Static member (A.K.A. class member) は、mixinの対象外。

Mixinはあくまでinstance fieldとinstance methodが対象となる。static member (static field, static method) は、classと同様にmixinのmemberとして定義はでき、instance memberからアクセスはできるが、その宣言によって生成されるmixinには含まれない。Mixinでstatic memberを定義することは稀。

リファレンス

仕様書

https://github.com/dart-lang/language/blob/master/accepted/2.1/super-mixins/feature-specification.md

公式サイトでの紹介

https://www.dartlang.org/articles/language/mixins

その他、個人による解説

https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3

まとめ

  • Mixinは、関数の合成のようにclassを合成する。(単一)継承の階層という制約では表現することが難しいデザインには、Mixinを適用する。
  • Dartでは言語レベルでMixinを採用しているため、安心してそれに依存できる。
  • Mixin Applicationのmember探索順序には明快な規則がある。
  • Dart 2.1において、Dart 1において報告されていた問題を解決するために、classから分離して新たにmixin専用のmixin宣言が採用されるなどして、仕様が改訂された。

フィードバックを歓迎します。コメントかtwitterでいただけるとありがたいです。