Edited at
DartDay 24

Dart型システムの動向

More than 1 year has passed since last update.


はじめに

Dartは2015年3月にそのVMをChrome他ブラウザに組み込むことを諦めて以来、JavaScript(JS)との相互運用性向上やプラットフォーム拡大をより重視するかたちで方向転換を行っています。結果として、DartはよりAOTコンパイルに適した言語になろうとしているように見えます。これにともない主に型システム周りで大きな変更をしつつ有るので、ここでまとめたいと思います。

Dartは元々、既存JSへの機能追加では解決できないJSの歴史的負債(Bool比較、thisスコープ、prototype chain、eval、...)を清算するために新規に開発されました。同時にコンパイルターゲットとしてのJSを強化することにも当初からコミットしています。機能はJSに素直にコンパイル可能なことを条件とし、JSと比較した強化機能もES6以降向けの提案を先取りする形で進められてきました(例:class、Promise(Future)、async/await、generator(async*、sync*)、import、tailing comma等)。型システムもJSに寄り添う形で実行時には変数が全て動的型付け(dynamic)です。つまり、変数の型はchecked modeでさえ完全にオプショナルで、単にIDEやAnalyzer等のツール向けのアノテーションでしかなかったと言えます。しかし、これが前述の方針転換の課題にもなっているようです。

Dartのもう一つの設計思想には、既存エコシステムの親和性の重視があります。これまでDartが考えていた親和性とは、ソフトウェア業界全体でみた開発者の多い既存言語(C、Java等)と構文的、意味的に類似性をもたせるということでした。Dartから外れますが、Androidでアプリ開発言語としてJavaを選んだのもそのためでしょう。しかし、新興プログラミング言語であるTypeScript、Swift、Kotlinを見る限り構文的、意味的類似性はさほど重要ではないようです。むしろ、モダンな言語機能を導入するためには構文的、意味的な変更を積極的に採用し、それが受け入れられているように思います。他方で、これら言語の共通点は各分野でデファクトとなっている言語との相互運用性が高いことでしょう。つまり、重要なのは、構文や意味論が近いことではなく、その分野のデファクト言語との相互運用性だった、ということで、Dartチームもそれに気が付いたようです。

参考にTypeScript、Swift、Kotlinがどれぐらい元気が有るかをGoogle Trendsで見てみます。

SwiftとTypeScriptの元気が良すぎて一見Kotlinが元気ないように見えますが、SwiftとTypeScriptを除いてElmを追加してみるとこんな感じです。

KotlinはDartより後発であるにも関わらずDartより元気があります。

Elmは当初とても元気が良かったのですが、早くも失速気味でしょうか。私はJS、HTML、CSSの既存資産との相互運用性に課題が有るから(と単なるBreaking Changeではないパラダイムシフトを強要するから)と見ています。

Dart頑張れ。


DartのAOTコンパイルのターゲット


Dart Dev Compiler (DDC)

ChromeへのDart VM組込みを諦めた代わりに取り組んでいるDart to JavaScript(ES6)トランスパイラです。既存のdart2jsと異なり、可読性の有るJSを吐くことを意識しています。また、アプリ単位ではなくモジュール単位でトランスパイルでき、これをJSから利用することが可能です。加えて、DartからのJS利用の改善(新JS相互運用ライブラリTypeScriptの型定義ファイル .d.ts の活用等)をすることでJS相互運用性の向上を図っています。なお、可読性のあるJSを吐くために既存のDartの意味論に一部制約を掛け、ほぼソース 前方 互換のまま型安全を実現するStrong Modeを導入しています。


Flutter

Dart Developer Summit 2015でプレアルファデビューしたAndroid、iOSのアプリ開発クロスプラットフォームです。当初はAndroidではVMiOSではAOTコンパイルしたネイティブコードをデプロイするものでした。iOS向けに効率的なAOTコンパイル結果を出力するために、ここでもStrong Modeが採用されています。現在はDart VMを変更してAndroid向けにもAOTコンパイルできるようにしていますが、さらにAOTコンパイル向けに独立した言語フロントエンド(New Front End)を開発中のようです。

他にも、Flutter絡みの修正(GCのレスポンス最適化、ホットリロードユーザメッセージ付きassert他多数)がDart SDK側にも取り込まれていることからも、本気で取り組んでいるプロジェクトのようです。

また、Googleからまだ正式発表のない謎のOS Fuchsiaでも一級市民としてFlutterが採用されているようです。FuchisaはMobile(Androidの代替?)に加え、PC(Chrome OSの代替?)やIoT(Android Things、Brilloの代替?)での活用も噂されています。Flutter、FuchsiaともGitHubのコミットは非常に高いペースを保っているので期待しています。


新興言語で破壊的に導入する言語機能 (型関係)

元気の有るTypeScript、Swift、Kotlinが、比較対象である各分野のデファクト言語(括弧内)に対して破壊的に導入した言語仕様の状況を確認してみます。Javaが既にそうであったKotlinの型安全、JSでは前も後もなかったTypeScriptの後置型修飾に括弧を付けていますが、要するに、ここで挙げた言語機能は、ここで挙げたすべての言語で全部取りです。

Dartも追随しようとしているようです。

機能
Dart
TypeScript
(JS)
Swift
(Objective-C)
Kotlin
(Java)
備考

型安全
×→✔


(✔)
Javaが既に型安全

Null安全
×→✔



TSは2.0から

型推論
×→✔


後置型修飾
×→✔?
(✔)


JSには型修飾が無い


Dartで導入される見込みの型機能


型安全 (Strong Mode) [実装済み]

Dart 1の仕様のままトランスパイルしようとすると、型安全でない JSをターゲットとした場合でさえヘルパー関数だらけになってしまします。


dart

var x = a.bar;

b.foo("hello", x);


JS

var x = getInterceptor(a).get$bar(a);

getInterceptor(b).foo$2(b, "hello", x);

これは、ディスパッチセマンティクスがDartとJSで異なるからです。例えば、該当するメソッドが無かった場合、Dartでは(あれば) noSuchMethod() を呼び出すか NoSuchMethodError をスローしますが、JSでは単にこれを無視します。

Strong Modeで型安全を確保し、barfoo() が有ることを担保できればDartと同一のJSを吐くことが出来ます。

Strong Modeは既に実装済みです。以前はDDCとともに別リポジトリで開発されていましたが、現在はDart-SDKのメインリポジトリにマージされています。Web上のお試し環境のDartPadにもStrong Modeは実装されています(右下のチェックボックス)。

なお、前述のソース前方互換は、今から積極的にStrong Modeを利用して慣れてもらうための措置、すなわちProduction Modeにいつでも戻れるでっかい避難口を設ける事で心理的安全を確保する措置だと思います。

他方で、一般的にはより重要なソース 後方 互換は 確保されない (Breakin Change)ので、Strong Modeがデフォルトになることが有るとすれば、Dart 2.0でしょう。


Null安全 (Non-null By Default (NNBD)) [DEPレビュー待ち、実装中]

Dart 1の仕様のままトランスパイルしようとすると、Null安全でないJSをターゲットとした場合でさえヘルパー関数だらけになってしまします。


dart

return (a + b) * a * b;  // a and b are declared as int



js

return dart.notNull(dart.notNull((dart.notNull(a) + dart.notNull(b)))

* dart.notNull(a)) * dart.notNull(b);

これは、null評価時のセマンティクスがDartとJSで異なるからです。Dartでは NoSuchMethodError をスローしますが、JSでは単にこれを無視します。

Null安全を確保し、ab がnullでないことを担保できればDartと同一のJSを吐くことが出来ます。

どっかで既に話しました?

Null安全については下記が詳しいです。

かくいう私もNull安全には無知で、DartにNon-null by Default とかあり得ないでしょ、と思っていたくちです。

しかし、上記を読んでその考えを完全に改めました。

Dartはもともと全てがオブジェクトで nullNull のインスタンスであり、 NullObject の直下のクラスです。すなわち int a = null は本来は代入互換性違反です。そうなっていない理由は、一つは前述の型安全をまだデフォルトとして導入していないからです。型安全を導入したStrong Modeでも int a = null が通るのは(Null ではなくて) null にすべてのクラスに代入互換性有り、という特別ルールを設けているからです。NNBDではその特別ルールを廃止します。簡単ですね。

ただ、こうしても Object だけは相変わらずnullableのままになってしまいます。そこで、新ルートクラス _Anything を導入し、その直下に NullObject とともに移動します。

ところで、Null をボトム(全てのクラスのサブクラス)にする提案もあり、Dart SDK 1.22で採用される予定です。

これは、NNBDの Null を新ルート直下に移動するという議論と将来衝突するかもしれない、と一見した時思いました。しかし、NNBDにおける _Anything - Null のブランチは Nullがボトムでもあります。問題は _Anything - Object のブランチで全ての非Null型のサブタイプとなり得る Nullに変わる何かが必要になるということです。このために Nothing (Null と同様に、_Anything とは異なってユーザから見える)を導入するようですが、これが新ボトムとして Null を引き継ぐのでしょう。さて、 Nothing は実際、いつ、どうやって使うのでしょう。おそらくそのインスタンスが必要となるケースはほぼ(?)無いはずです。Nothing はあくまで <Nothing>[]PopupMenuDivider implements PopupMenuItem<Nothing> のように、実コンテンツを持たないジェネリクス型インスタンスを生成するための型引数として利用されるのだと思います。

Null安全 (NNBD) は現時点でDart Enhancement Proposals (DEP)においてレビュー待ち(awaiting review)ですが、採用されることはほぼ間違いないように思います。Dart Developer Summit 2016でも紹介していました。また、本質的には後方非互換な変更ですが、それを極力抑えるように注意深く設計されています。提案書には移行戦略も記載されています。移行ステップは下記の通り。


  • (SDK) アノテーション @nullable_by_default@non_null_by_default の導入

  • (Tooling) 下記をサポート:


    • 構文のサポート (多くの構文糖衣を除く).

    • 静的チェック (上記アノテーションに従う)

    • 型オペレータ( ? , ! )の実行時サポートと動的チェック



  • (SDK) クラス階層のルート変更 (先述の _Anything 関係)

  • (Tooling) NNBDのグローバルなフラグ (ローカルなアノテーションに対してグローバル)


…の部分ですが、グローバルフラグの指定もなしにデフォルトでNull安全が実現されるのは少なくともDart 2.0になるでしょう。

ついでに、「null安全でない言語は、もはやレガシー言語だ - Qiita」で挙げていたNull安全をサポートする言語機能について見てみます。

言語

!, !!

?., ?->

?:, ??

map
flatMap

do 記法
?
型推論

Dart
×→✔

×→✔
×→✔

TypeScript

Swift





Kotlin





改めて見ると、Dartはそこそこ良い線いっています。JS対応待ちのTypeScriptよりもこの点ではリードしています。Dartに ?.?? を導入した当時は、なんて無意味なものを導入したんだろうと思っていましたが、これもNull安全への布石なのだと思えば納得です。


型推論 [実装済み]

null安全でない言語は、もはやレガシー言語だ - Qiita」を読んだ方にはもうおわかりと思いますが、Null安全と型推論は実質的に不可分です。これにはNull安全特有の事情もあります。Null型から非Null型への妥当な変換をもプログラマに依存するとしたら、システムは不当な変換を見抜けないことを、またレビューアは危険なコードを一見して見抜けないことを同時に意味し、Null安全的にはナンセンスです。また、Null安全の前提でもある型安全に共通の事情もあります。厳密な型修飾を強要する煩わしさからその言語が普及しないとしたら、結果的に型安全、Null安全を実現できませんので。これはDartのように動的型付けを起源とする言語の場合に、特に重要になるでしょう。つまり、動的型付け言語だから型修飾を省略できたというメリットを静的型付け言語でも享受するために機械が型を推論する、ということです。

ところで、業界有名人のHixieさんは var は従来通り dynamic と解釈されるべきとかなり強固に主張していますね。その理由が、JSからの開発者移行を重視するため、というもの。ここまで見てきた通り、構文的、意味的類似性はそこまで重要ではないと個人的には思います。それに、型安全、Null安全をどうやって実現しようとしているんでしょうね。Null安全は比較的新しい概念だからからか、Wikipedia(英語版)も全然充実していません。「null安全でない言語は、もはやレガシー言語だ - Qiita」を英訳して読んでもらいたいぐらいです。

ちなみに、HixieさんはHTML5のキーパーソンですが、いまやDartでFlutterなキーパーソンで、Flutterのマイルストーン名にまで登場します。


Union Type [検討中]

Union Typeの導入は2015年1月の段階で既に何度も話題に挙がっていたようですが、DEPにすら挙がっていません。しかし、先述の方向転換で状況は変わっています。Union Type無しでは、動的言語が享受してきた便利な言い回しか、せっかく導入した型安全やNull安全で得られるはずのメリットの一部のどちらかを犠牲にしなくてはならなくなるからです。したがって、Union Typeはなんらか形で導入されることになるでしょう。

例えば、Future<T>.then()(T a) -> dynamic 型のコールバックを引数に取ります。コールバックは実際には T または Future<T> のどちらでも返すことが出来ます。この柔軟さを維持しつつ型安全を導入する、すなわち T|Future<T> 型の必要性もUnion Type導入の一つの動機です。もっとも、Dart SDK 1.22ではどりあえず FutureOr<T> を導入するようです。

また、Null安全の ?int は内部的には int|Null のUnion Typeを想定しているようです。代案として Nullable<int> の構文糖衣とする案も検討していますが、既存言語システムに無用な複雑さを導入するという理由で先述のUnion Type案を推しています。ということは、NNBDの文脈で想定しているUnion Typeはタグ無しUnionですね、たぶん。

2017/01/11 追記

FutureOr<T> の仕様(仮)を見つけました。一見すると単なるジェネリッククラスですが、 T|Future<T> の歴としたUnion Types (のエイリアス相当)です。また、自動的にFlatteningされる点からも、ここでは明確にタグ無しUnionを想定していますね。


Generic Method [一部実装済み]

さきほどの Feature<T>.then() におけるコールバックの型 (T a) -> dynamic もそうですが、Dartの現状のジェネリクスはメソッドに関して貧弱でした。意図的にdynamicを指定しているのではなく、パラメタ型を指定できないのです。

Future then(dynamic onValue(T value), {Function onError})

これを補強しようというのがGeneric Methodです。

Future<S> then<S>(FutureOr<S> onValue(T value), {Function onError}) // 1.22~ ?

具体的には、Dart SDK 1.21リリース関連のBlog のGifアニメを見れば一目瞭然です。


後置型修飾 (Right Hand Types) [DEP提案中]

前置型修飾は可読性がよろしくないので、個人的には大歓迎です。自分がC言語を学んだ際、関数型他の複雑な typedef でコンパイラの気持ちを理解するまでひとしきり時間を要したのを思い出します。

また、前置型修飾の表現力の問題もあります。

typedef int Comparator(int a, int b);

Comparator c;

とは書けますが、同じことを

int c(int a, int b);

とは書けません。関数定義と思われてボディが無いと怒られていしまいます。

var c: (a: int, b: int) -> int;

なら行けそうです。

しかし、これ、どうなんでしょうね。というのも、


  • Breaking Change自体が目的であるし、

  • DEPではawaiting reviewにもなっていないので。


    • とはいえ、DEPに挙がっている時点で見込みは有るし、 (Union TypesはDEPに挙がってもいない)

    • DEPで門前払いのClosedにされた多くの提案に仲間入りしていないだけマシかも。



少なくとも、NNBDと類似の移行戦略は必須になるでしょうね。

希望もありそうです。

関数の名前付きオプション引数のデフォルト値区切りには従来はマップリテラルのアナロジーから : を用いてきましたが、Dart SDK 1.21では新たに = も利用可能になりました。

enableFlags({bool hidden: false}) { /* … */ }

enableFlags({bool hidden = false}) { /* … */ }  // Dart SDK 1.21以降はこれも可能

こんなしょうもないエンハンスをマイナーバージョンに突っ込む動機は、時間を稼ぎつつ将来的に識別子宣言の右側から : を一掃するつもりではないでしょうか。つまり、そこに: で分離した後置型修飾を置く布石と。ちょっと、穿った見方をしすぎですかね。

2017/01/09 追記

Right Hand Typesは「近いうちに採用される予定はないと思う」、とDartチームのメンバが言っていました。

代わりに(?)、1.22で関数の型宣言シンタックスを拡張するようです。

名前付きオプション引数のデフォルトセパレータ : は既にほとんどDeprecatedでした。


Dart VMへの考慮 [2017/01/15 追記]

冒頭でDartはよりAOTコンパイルに適した言語になろうとしていると書きましたが、引き続きDart VMも重要な役割を担います。サーバーサイドDartはVM上で動作しますし、Dartiumもあります。また、Dart SDKのツール群はそのほぼ全てがDartで実装され、VM上で動作します。他にもVMを選択する積極的な理由があります。VMにおけるウォームアップ(JIT最適化コンパイル)後のネイティブコードはAOTコンパイルによるネイティブコードよりもずっと高速に動作するようです。ちょっと驚きですが、JITなら実行時に収集する派生型情報を利用できるからとか。

このDart VMの存在がAOT向けの型機能拡張に、他の言語にはない微妙な影響を与えているようです。VMでは基本的にコンパイルの全てのステップが実行時に行われます(スナップショットは有りますが)。特にスタートアップ時にはパースや静的チェックの多くが行われるので、これらの高速性が重要となります。型推論のような比較的重い処理をVMが単に無視することができれば、そのままproduction modeで実行できます。先述のStrong Modeにおける前方互換性のもう一つの理由がここにあるようです。NNBDやジェネリックメソッドの様な構文の拡張(前方非互換)も、単に読み飛ばすことが出来ればそれにこしたしたことがありません。

幸いなことに、新たに拡張される型機能の多くはAOTコンパイルでこそ意味が有るものであり、その他もIDEと連携したAnalyzerがコーディング時に行うことが出来ます。実際に現在のVM上では拡張型機能の多くが単に無視され、JITコンパイル時の最適化で効果があるもの(実行時チェックコードの省略等)だけが解釈されるようです。


さいごに

ChromeにVM組込みを諦めて以来、Dartの人気はだだ下がりで残念ですが、Googleは諦めるどころか重要なプロジェクトに位置付けているようです。それは開発リソースに現れていて、GitHubにおける開発活性度は2016年平均で150 commits/week程度とハイペースです。これはswift、kotlinのそれにほぼ匹敵し、TypeScriptの約倍です。また何と言っても、世界で最も稼いでいるであろうWebシステムのトップ2、AdWordsAdSenseの大事な顧客(スポンサー)向けUIの再構築で利用したことに、DartのGoogle内でのポジションが現れています。まあ、だからと言って一般に普及する理由にはなりませんが、少なくともその必要条件である継続的なエンハンスが期待できます。

また、AndroidでJavaに変わるべき言語として、Kotlin、Swift、C#(Xamarin)、Go、Rust等、みなさん好き勝手言っていますが、Dart以外が市民権を得るのは現実的ではないでしょう。Androidは「オープン」であると同時に「完全」にGoogleのコントロール下にあります。そのアプリ開発言語も然り。GoogleのAndroidにおける影響力は、誰のものでもないWebにおけるそれとは比べ物にならないでしょう。そしてGoogleはFlutterにも相当のリソースを掛けており、50 commits/weekぐらいでしょうか。先述のHixieさんの位置付け(マイルストーン名)も象徴的です。他方で、他の言語をAndroidに持ち込むことに投資している様子はありません。

ところで、ソフトウェア開発コミュニティ全体に及ぼす影響力はモバイルの開発者コミュニティがWeb開発者コミュニティを凌いでNo.1では無いではないでしょうか。これは、開発者人口からはほぼiOS専用であろうObjective-Cと、同じくその置き換えであるSwiftがTIOBE Indexにおいて10位前後で仲良く肩を並べていることからの推測です。二大モバイルOSのもう一方、AndroidでDartが一級市民になれば(仮に、iOSとのクロスプラットフォームとして認められなくとも)、Swiftと同様に急速に知名度が上がるはずです。そうなればDartのお家芸であるWeb開発業界、ひいてはソフトウェア開発業界全体でも知名度が上がりTIOBEでJSと肩を並べる日も遠く無いように思います。

※ TypeScriptが50位以内にランクされないTIOBEにどれほどの意味があるかはさておき。

2017/01/04追記

Hixieさん曰くStack Overflow Developer Survey 2016 Resultsによると、Web開発者はモバイル開発者の3倍居る、とのことです。


蛇足


undefined

Nothing で思い出したのですが、Dartには一時期 undefined がありました。これは、関数のデフォルト値無しのオプションナル引数が省略された場合に取りうる特別な値でした。クラス階層を無視したハンドクラフトな値だったようですが、別の関数に引き渡す際にはさらなる特別扱いが必要になったとか。必要性が希薄なわりに無用な複雑さをどんどん増し兼ねないということからDart 1.0以前に仕様から削除されました。そんなこんなで、先述の「Nothing」を undefined と命名するのは歴史的にも、JSのアナロジー的にも無しですね、きっと。


ChromeとかWebAssemblyとかOilpanとか

Chrome 50でDOMオブジェクトのメモリ管理方法が被参照カウンタからGCに変わったことをご存知でしょうか。この長寿だったプロジェクトはOilpanと呼ばれ、日本人のharakenさんがリードしていましたが、その歴史的な開始動機はDart VMをChromeに搭載することだそうです。先述の通りDart VMの組込み計画はなくなりましたが、Oilpanのお陰でChromeの性能(速度、メモリ)は向上し、Use-after-free in Blink な賞金首は鳴りを潜めました。

ところで、今考えるとDart VMをChromeに搭載しなくて良かったと思います。というのも、Oilpan導入後もV8(JS)のGCとBlink(DOM)のGCは連携した別々のGCであり、今でもより良い連携を模索しています。それだけ複数のGCを持つというのは難しいことなんだと思います。かつてはブラウザ上で全盛を誇ったFlashも、いまやブラウザたちに煙たがられて静かに死を待つのみですし、.NET(CLE)に至っては...

ブラウザに別言語の実行環境を持ち込むといえばWebAssemblyも有りますが、こちらはプログラマがメモリ管理をする必要がある言語が当面のターゲットです。加えて、WebAssemblyの処理系バックエンド(下層部分)はJSのそれと共通化できるように最初から考慮されています。Chromeで言うとTurbo Fan以下がWebAssemblyと共通になるようです。GCがない言語であっても、処理系は共通化すべき(複数持つべきではない)ということだと思います。

なお、WebAssemblyは将来的にはGC言語をサポートするようです。そうなると、Dartを「ネイティブ」にブラウザで実行しようという機運が再び高まることでしょう。