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のものを優先するかには明快な規則がある。
- まずは、mixin applicationを継承したclassが探索される。
- 次に、後から追加したmixinのmemberが優先して探索される。
- 最後に、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");
となる。
もし、C
にaMethod()
が定義されていなければ、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
は、S
にM1
, M2
, M3
を適用したmixin applicationなので、最後に適用されたM3
のmemberが最も優先して探索される。 (規則通り、S
のmemberの優先度は最も低いことに注意。)
さらに、Mixin Applicationのclassに、さらにmixinを追加することも可能。
mixin M4 { String aMethod() => "M4 method"; } class CSuper2 = CSuper with M4;
CSuper2
は、CSuper
にM4
を適用した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でいただけるとありがたいです。
Dart 2.1の概要と所感
Dart 2.1がリリースされた。
https://medium.com/dartlang/announcing-dart-2-1-improved-performance-usability-9f55fca6f31a
Flutterからのユーザーフィードバックからの改善が中心という印象。 Dart 2.0において、数年に渡り開発チームがかかりきりになっていた、型安全性を強めた型システムの安定版がリリースされたため、これからはよりマイナーな改善に注力していくと思われる。
Dart 2ではクライアントサイドのユースケースに注力していくとアナウンスされたとおり、WebアプリとNativeアプリ両方の生産性を高めていくことが強調されている。 Flutterは無事離陸できた感があるので、WebアプリとしてAngularDartのパフォーマンスと生産性の魅力がもう少し知られたら良いし、そしてもし取り組まれるならば年単位の大仕事になるだろうが、Web用Flutterの動きに期待したい。
数値リテラルの改善
FlutterユーザーからのFBベースの改善とのこと。
TextStyle(fontSize: 18.0)
のdouble型リテラルとして18.0
と書かなければstatic errorになっていたが、TextStyle(fontSize: 18)
と、18
と書いてもdouble型と認識してくれるようになった。
Mixinサポートの改善
Mixinの専用構文として、class
の代わりに使用するmixin
キーワードが導入された。以前は、class
キーワードを用いていくつかの制約のもと、mixin applyできるclassを作成していたが、mixin用に構文が分離された。
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {}
extends
句の代わりに、on
句を使用する。上記の例では、FlutterのState
classのみがこのmixinを適用する事ができる制約となる。(mixin bodyでは、super
でsuper classのmethodを起動できる。super.dispose();
など。)
Future
とStream
がdart:core
から利用可能に
Future
とStream
を解決するために、大多数のファイルにいちいちimport 'dart:async';
と書かなくてもよくなった。
ただし、dart:async
がすべてexportされたわけではないので、StreamController
やCompleter
を使いたい場合は、いままでどおりdart:async
のimportが必要。
パフォーマンス向上
In a few edge cases, though, the comprehensive checks added by the new type system caused an undesirable overhead of 20–40%. In Dart 2.1 we’ve greatly reduced the cost of the type checks, both for AOT-compiled code and for code run in the VM with JIT (just-in-time) compilation.
たぶん、Reified Genericsを導入した際のパフォーマンス劣化なのだろうなと想像するが、いくつかのエッジケースでパフォーマンス劣化をかなり緩和したとのこと。 また、dart2jsでもコードサイズを17%、コンパイル時間を15%削減できたとのこと。すでにかなりコンパクトなコードサイズだったので満足していたが、そこからさらにコードサイズを17%削減はすごい改善。
Protocol Buffers
Dartサポートが紹介されている。
https://developers.google.com/protocol-buffers/docs/darttutorial https://github.com/knative/docs/tree/master/serving/samples/helloworld-dart
GoやPythonなみの第一級言語としてのサポートと普及が期待できる。 はやくGCP全体でDartサポートが進んでほしい。
その他バグフィックスなどの改善
- Flutterにおいて、コンパイル時にエラーとなるべきコードがそうならなかった不具合の修正や、いくつかのうまく動いていなかったLinterがちゃんと動くようになったなど、バグフィックスがされている。
- dart:htmlの新し目のWebAPI、たとえばServiceWorker APIにかなりのfixが入った。あとで試してみたい。
検討中
https://github.com/dart-lang/language にて言語の改善の議論が公開されている。
- Setリテラルや、constを作る際の制約の緩和など。2.0には結局間に合わなかった、NNBDというNon Null Typeをデフォルトにする大型の破壊的変更にも着手している。
- リスト内包表記のような提案もある。 https://github.com/dart-lang/language/issues/78 FlutterのWidgetをcomposeする際には便利になりそう。
- JavaScriptのようなSpread Operatorも導入が検討されている。https://github.com/dart-lang/language/issues/47
普通の型安全な型システムがデフォルトになってしまったし、仕様が膨らんでいって、良くも悪くも普通の言語になっていく印象。
Optional Semicolon
KotlinのようなOptional Semicolonを検討中とのこと。 https://github.com/dart-lang/sdk/issues/30347
かなり議論が紛糾した経緯がある。 https://github.com/dart-lang/sdk/issues/30347
個人的にはStatement separatorに曖昧なルールを導入してしまうのは反対なのだが...非採用になってほしい。
そんな危険なものよりも、finalとvarをoptionalにして、そのかわりにassignemntに、finalには=
を、varには:=
を使う記法を採用してほしい。
AngularDartでHot Reloadが使えるようになっていた
FlutterのウリのひとつであるHot Reload、この体験がAngularDartでも可能となっていた。WebでもHot Reloading。
dartファイルの編集
htmlファイル(angular template)の編集
cssの編集
これで、とくにHTMLとCSSの編集がかなり捗るようになる。
--hot-reloadオプションをつけて起動するだけで、その他の設定を変えずに有効になった。
webdev serve --hot-reload
従来のLive Reloadでは、コードの編集からブラウザのリフレッシュを経てのコードの反映までに5秒から10秒かかり、ブラウザのタブがリロードされることでランタイムの状態が一新されていたが、Hot Reloadでは0.5秒から2秒くらいで差分更新され、その他のランタイムの状態が維持される。
ただしまだ不安定な印象。手元のかなり大きなコードベースでは、ランタイムエラーがでたり、なぜかhot-reloadの動作の直後にlive-reloadの動作になりブラウザのタブがリロードされてしまう。
従来のLive Reloadでも便利なので、安定するのを気長に待ちたい。
webdev serve --live-reload
安定したら公式が大々的に宣伝をしそう。
AngularDart - UI Componentのアニメーションを維持したままページ遷移する
やりたいことは以下の動画のとおり。
ページ遷移してURLは変わるが、アニメーションは維持する。
モチベーション
Routerでページ遷移すると、対応するComponentが新たに起動する。しかし、上記の動画のように、たとえばTabの遷移に合わせてURLを変更したい場合、Component instanceが新たに起動して状態が一新されると、UI上はアニメーションがぶつりと中断してしまい不格好なUIになってしまう。アニメーションを維持したままページ遷移したい。
ソリューション
Tab1 page, Tab2 pageに対応するpathに紐つけるPageのComponentを、同一のものにする。ここでは、ATabCyclePage
とする。
そして、Routerのlife cycle interfaceであるCanReuseを、Componentにimplementする。CanReuseは、Component instanceを再利用するかどうかを指定する。
class ATabCyclePage implements OnActivate, CanReuse { int activeTabIndex; @override void onActivate(RouterState previous, RouterState current) { activeTabIndex = _isTab1Page(current.path) ? 0 : 1; } @override Future<bool> canReuse(RouterState current, RouterState next) async { // Componentを再利用する条件を記述。たとえばcurrentとnextのpathを比較してUIのTab遷移に対応する状態ならばtrueを返す。 } }
Templateのサンプル
<material-tab-panel class="tab-panel" [activeTabIndex]="activeTabIndex" (tabChange)="handleTabChange($event)"> <material-tab label="Tab 1"> <template deferredContent> <ng-container *ngIf="activeTabIndex != null"> <material-button raised (trigger)="navigateToTab2Page()">click to Tab 2</material-button> </ng-container> </template> </material-tab> <material-tab label="Tab 2"> <template deferredContent> <!--Tab 2 Page--> </template> </material-tab> </material-tab-panel>
Tabが変わった際のハンドラ
void handleTabChange(TabChangeEvent event) { if (event.newIndex == 0) { _navigateToTab1Page(); } else if (event.newIndex == 1) { navigateToTab2Page(); } else { // throw } } void _navigateToTab1Page() => _router.navigate(RoutePaths.tab1 .toUrl(parameters: {'id': _router.current.parameters['id']})); void navigateToTab2Page() => _router.navigate(RoutePaths.tab2 .toUrl(parameters: {'id': _router.current.parameters['id']}));
これで、アニメーションを維持したままページ遷移が可能となった。
Firebaseでチーム開発の際のプロジェクト作成、運用について
Firestoreを使ったアプリでチーム開発するとき、各自のローカル環境で開発するにはどうすればいいんだろう。GAEのようにローカルで動作するエミュレーターはないみたいだから、チームメンバーごとに開発用Firebase projectをつくるの??
— ntaoo (@ntaoo) October 14, 2018
3名のメンバーでアプリを作るとしたら、1アプリごとに3名分+dev+staging+production用で6プロジェクト必要。そして仮に10アプリをGCP organizationで管理すれば60プロジェクトになる。これははたして想定内なのか?
— ntaoo (@ntaoo) October 14, 2018
authや他の機能もあるから、チームメンバーののローカル開発用にそれぞれprojectを作るのは仕方ないのか。staging serverのようにうまく使いまわして節約していくべきだな。
— ntaoo (@ntaoo) October 14, 2018
まあそういうことですよね
— ntaoo (@ntaoo) October 14, 2018
- ローカルコンピュータで開発する際も常時Firebaseと通信できる環境が必要。
- ひとつのアプリに対して、(Dev x チームメンバー数) + Staging + ProductionのFirebase Projectを作る。チームメンバーが3人ならば5プロジェクト。加えて、CI用のプロジェクトも必要かもしれない。
- Dev用プロジェクトは、可能な状況ならばチームメンバー数よりも少なくしてもよいかもしれない。しばらく使用しないならば開放して他のチームメンバーに割り当てる。
Dartのプロジェクトジェネレーター Stagehandを使おう
Stagehand = 舞台係。裏方。
手動で作るのは辛いのでツールの力を使う。Staghandは、パッケージの雛形を生成してくれる。
https://github.com/dart-lang/stagehand
使い方
上記のリンクにすべて書かれている。
インストール
pub global activate stagehand
パッケージ生成
mkdir package_name cd package_name stagehand package-simple
Angularアプリの生成
Angularアプリもパッケージである。
mkdir angular_app_name cd angular_app_name stagehand web-angular
コマンドラインアプリの生成
コマンドラインアプリもパッケージである。
mkdir console_name cd console_name stagehand console-full
サーバー、Angularを使用しないWebアプリ、StageXL(Canvas)のアプリ
すべて同じ方法で生成してくれる。
Flutter
Flutterは例外で、独自のプロジェクトジェネレーションコマンドをもつ。
flutter create a_new_project_name
Dart BuiltCollection, BuiltValueの使いかた 基本編
残念なことに、DartのコアライブラリにImmutable Collectionライブラリは無いが、BuiltCollection, BuiltValueパッケージを使えばImmutable Collectionを扱うことができる。ここでは、それらの使い方の基本を簡単に解説していく。
今回のサンプルコード
https://github.com/ntaoo/built_collection_value_practice
Mutable Collectionの問題
DartのコアライブラリのListは他のほとんどの言語と同じくmutable List。
class C { C(this.name); String name; } test('Standard List is mutable.', () { final c = C('a'); final l1 = [c]; final l2 = List.from(l1); expect(l1 == l2, isFalse, reason: 'Shallow comparison.'); expect(l1.first == l2.first, isTrue, reason: 'They share the same element.'); c.name = 'b'; expect(l1.first == l2.first, isTrue, reason: 'They share the same element.'); expect(l2.first.name, 'b', reason: 'They share the same element.'); });
https://github.com/ntaoo/built_collection_value_practice/blob/master/test/built_collection_test.dart
上記のコードのように、うっかりとmutable elementの参照値を複数のListで共有して、意図せぬ挙動に悩まされた経験はだれもがしているだろう。 Immutable CollectionとValueを組み合わせると、意図せずそのようなコードを書いてしまう心配がなくなる。
Built CollectionとBuiltValueの特徴
Built Collectionは、Immutable Collectionを提供するパッケージ。BuiltValueは、Valueを提供する。
Built Collectionは、Collectionのmutationではなく、elementの追加、変更、削除のたびに新しいCollectionを生成するため、意図せずCollectionをmutationするありがちな不具合を防げる。
また、BuiltValueと組み合わせれば、Collectionのelementまで含めたdeep comparisonが容易になり、そのパフォーマンスが良い。
さらに、変更したelement以外は変更前のListのelementの参照を引き継ぐため、効率的なコードを簡単に書くことができる。
他の選択肢
準コアライブラリとも言えるpackage:collectionにはUnmodifiableListView
があり、生成後のmutation操作ができなくなる。また、コアライブラリのList.unmodifiable
コンストラクタを使えば同様になる。しかしImmutable collectionからその一部のelementを変更したImmutable collectionを作るといった操作は提供されていない。
また、package:collectionには、elementがValueでなくともコアライブラリのCollectionの同一性を確かめるライブラリが提供されている。
BuiltCollection, BuiltValueのようなImmutable Collection / Valueを必要としない、またはコアライブラリのCollectionを使用するべき理由があれば、こちらの選択肢を検討する。
名前の由来
Builder patternで表現しているため、Built Collection, Built Value。
他言語におけるImmutable Collection
Mutable collectionに加えてimmutable collectionも提供している言語は、Scala, C#(.NET), Java(Guava), Racket、等々。
Haskellはimmutable collectionしか提供していない。PHP, Python, Ruby, JavaScriptなどのスクリプト言語では自分が知る限り提供されていない。
BuiltCollection, BuiltValueの基本的な使い方
Built CollectionにおけるImmutable ListであるBuiltListを例に、BuiltCollection, BuiltValueの使い方を紹介する。
class C { C(this.name); String name; } test( 'BuiltList provides deep comparison, but the non BuiltValue elements can still be mutable.', () { final c = C('a'); final l = [c]; final l1 = BuiltList<C>(l); final l2 = BuiltList<C>.from(l); expect(l1 == l2, isTrue, reason: 'thanks to the deep comparison.'); expect(l1.first == l2.first, isTrue, reason: 'they share same element.'); c.name = 'b'; expect(l1.first == l2.first, isTrue, reason: 'they share same element.'); expect(l2.first.name, 'b', reason: 'mutated, because they share the same mutable element. NOT recommended.'); });
https://github.com/ntaoo/built_collection_value_practice/blob/master/test/built_collection_test.dart
生成
final c = C('a'); final l = [c]; final l1 = BuiltList<C>(l); final l2 = BuiltList<C>.from(l);
コンストラクタが用意されている。
比較
expect(l1 == l2, isTrue, reason: 'thanks to the deep comparison.'); expect(l1.first == l2.first, isTrue, reason: 'they share same element.');
Mutable Listとは異なり、==
でdeep comparisonができている。ただし、
c.name = 'b'; expect(l1 == l2, isTrue, reason: 'they share the same element.'); expect(l1.first == l2.first, isTrue, reason: 'they share the same element.'); expect(l2.first.name, 'b', reason: 'mutated, because they share the same mutable element. NOT recommended.');
c
がmutableなelementである場合、BuiltListでも同一判定されるにもかかわらず、実際はmutationしてしまう。そこで、BuiltValueと組み合わせることが推奨されている。
BuiltCollectionをBuiltValueと組み合わせる
test('BuiltList can be deeply immutable with built values.', () { SimpleValue buildValue() { return SimpleValue((b) => b ..anInt = 1 ..aString = 'a' ..aListOfStrings = ListBuilder<String>(['one', 'two', 'three'])); } final l1 = BuiltList<SimpleValue>.from([buildValue()]); final l2 = BuiltList<SimpleValue>.from([buildValue()]); expect(l1 == l2, isTrue, reason: 'thanks to the deep comparison.'); expect(l1.first == l2.first, isTrue, reason: 'value comparison.'); final l3 = l2.rebuild((b) => b.first = b.first.rebuild((b) => b..aString = 'b')); expect(l2 == l3, isTrue); expect(l1 == l2, isFalse, reason: 'l2 was rebuilt.'); expect(l1 == l3, isFalse, reason: 'l3 is l2.'); expect(l2.first.aString, 'b'); expect(l3.first.aString, 'b'); });
SimpleValueはBuiltValue。
l2をrebuildし、そこでvalueをrebuildしている。期待通りl1 == l2
がfalseとなった。
rebuildで変更
final l3 = l2.rebuild((b) => b.first = b.first.rebuild((b) => b..aString = 'b')); expect(l2 == l3, isTrue); expect(l1 == l2, isFalse, reason: 'l2 was rebuilt.'); expect(l1 == l3, isFalse, reason: 'l3 is l2.');
var newList = list.rebuild((b) => b ..add(4) ..addAll([7, 6, 5]) ..sort() ..remove(1));
BuiltCollectionのインターフェース
test('BuiltList is not a List, but an Iterable', () { expect(BuiltList<int>([1]), isNot(isList)); expect(BuiltList<int>([1]) is Iterable, isTrue); });
BuiltListにList Interfaceがない。add
, addAll
などのListの変更のためのメソッドは提供されていない。
BuiltListにInterable Interfaceはある。
test('BuiltList can return self as standard Immutable List', () { final list = BuiltList<int>([1, 2, 3]).asList(); expect(() => list.add(4), throwsA(TypeMatcher<UnsupportedError>())); expect(() => list.first = 4, throwsA(TypeMatcher<UnsupportedError>())); });
ただし、コアライブラリListを返すことはできるが、変更を試みるとランタイムエラーが発生する。
BuiltValue
生成
SimpleValue build() { return SimpleValue((b) => b ..anInt = 1 ..aString = 'a' ..aListOfStrings = ListBuilder<String>(['one', 'two', 'three'])); }
コンストラクタでクロージャを使って、メソッドカスケーディングで値を組み立てる。 各fieldの指定漏れはランタイムエラーとなる。標準のfinal field指定では静的解析の支援を受けられたが、BuiltValueでは受けられない。 Filedの数が10以上に増えてきた場合は、別途factory named constructorを用意して静的解析できるようにすることも検討したらよい。
BuiltValueのfieldにはデフォルトではnullを許容しない。Non Nullable By Default。 @nullable annotationを付与するとnullを許容する。
比較
test('It is a value.', () { final s1 = build(); expect(s1.anInt, 1); expect(s1.aString, 'a'); expect(s1.aListOfStrings, ['one', 'two', 'three']); expect(s1.aListOfStrings, BuiltList<String>(['one', 'two', 'three'])); final s2 = build(); expect(s1, s2); });
BuiltValueはValueなので、deep comparisonで同一object判定される。 言うまでもなく、BuiltValueでない通常のobjectの比較では、異なるobjectと判定される。
rebuildで変更。
test('can rebuild', () { final s1 = build(); final s2 = s1.rebuild((b) => b ..anInt = 2 ..aString = 'b'); expect(s1 == s2, isFalse); expect(s2.anInt, 2); expect(s2.aString, 'b'); expect(s2.aListOfStrings, BuiltList<String>(['one', 'two', 'three']), reason: 'it still holds the privious value.'); });
BuiltValueのNest
group('NestedValue', () { SimpleValueContainer build() { return SimpleValueContainer((b) => b ..simpleValue.aString = 'a' ..simpleValue.anInt = 1 ..anInt = 1); } test('can build nested values.', () { final c1 = build(); expect(c1.anInt, 1); expect(c1.simpleValue.anInt, 1); expect(c1.simpleValue.aString, 'a'); }); test('can rebuild nested values.', () { final c1 = build(); final c2 = c1.rebuild((b) => b ..simpleValue.anInt = 2 ..simpleValue.aString = 'b'); expect(c1 == c2, isFalse); expect(c2.anInt, 1, reason: 'it still holds the previous value.'); expect(c2.simpleValue.anInt, 2); expect(c2.simpleValue.aString, 'b'); }); });
BuiltValueのfieldがBuiltValueの場合、コンストラクタ引数のクロージャの中では、またクロージャを指定せずとも、
SimpleValueContainer((b) => b ..simpleValue.aString = 'a' ..simpleValue.anInt = 1 ..anInt = 1);
のように書ける。rebuild()でも同様。
Serialization, Deserialization.
BuiltValueにはSerialization, Deserializationのストラテジが用意されており、StandardJsonPluginを組み合わせるとJSON string互換のMapと変換できる。
https://github.com/ntaoo/built_collection_value_practice/blob/master/lib/serializers.dart
test('can serialize to json Map.', () { final s1 = build(); final result = standardSerializers.serialize(s1); expect(result is String, isFalse); expect(result is Map, isTrue); expect((result as Map)['\$'], 'SimpleValue'); expect((result as Map).keys.toSet(), Set.from(['\$', 'aString', 'anInt', 'aListOfStrings'])); expect((result as Map)['aListOfStrings'], isList); }); test('can serialize to json Map, with familiar format.', () { final s1 = build(); final result = standardSerializers.serializeWith<SimpleValue>( SimpleValue.serializer, s1); expect(result is String, isFalse); expect(result is Map, isTrue); expect((result as Map).containsKey('\$'), isFalse); expect((result as Map).keys.toSet(), Set.from(['aString', 'anInt', 'aListOfStrings'])); expect((result as Map)['aListOfStrings'], isList); });
メンテナンス
BuiltCollection, BuiltValueはsource_genでソースコードジェネレーションしているので、使用の際にはbuildが必要となる。
pub run build_runner build
ファイルの変更を検知して自動的に再ビルドする場合は、watch
を使用する。
pub run build_runner build watch
Flutterの場合は、flutter packages
を付加する。
flutter packages pub run build_runner build
ユースケース
- RPCのデータ送受信
- Http request/response
- Firestore request/response
- UIでの表示のためのModelからのデータ送信
- 並列プログラミング
- その他、高頻度でListをmutationしパフォーマンスが必要なユースケース以外のすべて
欠点
- 構文の簡潔さはList literalにはもちろん及ばない。
- 一般的ではない(not familiar)ため、習得コストがかかる。
- コアライブラリではない。したがって、開発したライブラリの公開APIには代わりにIterableを指定できないか検討する。BuiltListはIterableインターフェースを実装している.
- 高頻度でCollectionの中身を書き換えたい場合はMutable Collectionよりもパフォーマンスが悪い。
したがって、初心者向けではないかもしれないかもしれない。それに、我々はMutable Listの動作に慣れきっているので、いきなりImmutable Listを強制すれば混乱して生産性が落ちそうだ。 しかしImmutable Listを第一の選択肢とすれば、つまらない不具合に悩ませられる機会が大幅に減るだろう。
いつかはImmutable Listがコアライブラリに取り込まれてほしい。