JavaScript Advent Calendar 2017 - Qiitaの第一日目です。JavaScriptにおいてメタプログラミングを用いて、エンジニアリングにおける邪悪と闘うという趣旨の記事です。
秋のJavaScript祭 in mixi 2017 - Javascript祭りでこれからのメタプログラミングJavaScriptの正義を語ろうという発表をしてきました。この記事はそれをベースにしています。
技術書典3で頒布した簡単JavaScript AST入門という同人誌ですが、どうも電子版を求める声が多いので、簡単JavaScript AST入門 - 東京ラビットハウス - BOOTH で電子版を販売しています。
結論から
メタプログラミングを活用すればこの三つを達成できます。
- コーディングタイムコンパイルという新しい概念・技術
- 暖かみ溢れる手作業を根絶
- 生産性を向上して、相対賃金を上げる
生産性とは
まず生産性とは何か?をはっきりさせましょう。
同じ時間をかけて、倍の量のコードを書くと生産性は二倍なのでしょうか?それは正しいかもしれないし正しくないかもしれません。
同じ時間をかけて、同じ量のコードでも、問題解決への貢献度合いが二倍であれば、それは生産性が二倍だと言っていいでしょう。エンジニアは「達成したい課題・問題を解決する」ためにいるので、エンジニアが生産性を語るのであれば、単位時間あたりの問題・課題解決への貢献量だと言えます。
エンジニア 生産性でぐぐると色々ヒットしますが、一般的にエンジニアの生産性はピンキリで、優秀なエンジニアは平均的なエンジニアよりも何倍、あるいは一桁・二桁違うといわれるほどの生産性を持つとされています。
エンジニアリングの世界には邪悪が満ちている
生産性に差が出る現象には、エンジニアリングの世界に邪悪が満ちているからなのではないでしょうか?
別にJavaScriptに限りません。Java, PHP, Ruby, Scala, Golang など、さまざまな現場でこれらの邪悪は実際にあるものです。邪悪に犯されずにプログラミング出来ている人は大変幸せなのです。
では、これらの邪悪は何が引き越してるのでしょうか?いくつか要因はありますが、おおざっぱに抽出すると大抵は「複雑性」に帰結します。人間の頭の限界を超えた複雑性はそれだけで邪悪なのです。
邪悪と闘う
メンバーの頭でちゃんと理解できる程度にはシンプルにするというのが邪悪と闘う唯一の術です。
- クラスやメソッドの責務を減らす (単一責務が理想)
- 疎結合にする
- 分割統治する
もちろん、扱っている問題が複雑である以上どうしても、シンプルさとは馴染まない部分もありますが、古今東西のプログラマは構造化プログラミング、オブジェクト指向、関数型言語、デザインパターンなど様々なやり方で複雑性と闘ってきました。僕が個人的に設計論で一番重要なのは名前であると思っていますが、そういった設計論の詳細は今回の記事では置いておきましょう。
多くのプログラマの共通認識として重複は邪悪を生み出す、あるいは邪悪そのものであるというものがあると思います。そこで、様々な方法論があるなかでも、僕が注目したいのはDRY原則です。
DRY原則
もともと達人プログラマーという本に出てきた概念ですが、DRY原則が有名になったきっかけはRailsでしょう (間違ってたらご指摘ください)。Railsは、「設定を書く」ことすらDRYに反するので、設定よりも規約を重視しようというような形で、ひたすらDRYを追求したフレームワークです。
DRY原則は Don't Repeat Yourself
の略ですが「あなたの手で繰り返してはいけない」という意味です。
なら話は簡単です。「あなたの手」じゃなければ繰り返してもいいんです。幸い我々には繰り返しが得意、かつとても正確なヤツがいるではありませんか?わかってると思いますがそれはプログラムです。「あなたの手」で書かれるものがDRYでさえあればいいのです。そこでメタプログラミングの出番です。
メタプログラミング
メタプログラミングというのはプログラムを扱うプログラムのことで、プログラムを生成・解析・加工するものです。
静的と動的
メタプログラミングは主に静的なものと動的なものに分ける事ができるでしょう。
コンパイラやプリプロセッサは静的なものです。その中でもC++のテンプレートやScalaのマクロなんかはユーザーにとって身近なメタプログラミングです。これの利点は最終的に複雑なコードを、それよりは低い複雑性で実現できることとあらかじめ静的に処理するため、実行時の負荷 (CPU, メモリ, I/O含めた全ての負荷) がないことです。
RubyやJSのようなLL言語のメタプログラミングは大抵のケースでは動的なものです。主にリフレクション機能や型の緩さを悪用活用したものです。
これらはどちらも黒魔術めいたものになりがちです。前者はメタプログラミングのための言語仕様が、本来の言語とは違うゆえの不便さが大きく、後者は動的に変わる振る舞いによる難しさによるものです。
JavaScriptのメタプログラミング
さてJavaScriptの話に絞りましょう。JavaScriptは他の言語とは違う特異な歴史を持つため少し違った進化をしました。普通はコンパイラの内部表現に過ぎないASTがESTreeという形で標準化されています。そのためASTを操作するためのツールやノウハウが充実しています。
そして、皆さんのプロダクトでも最近ではトランスパイラ(Babel)を導入する事例が多いでしょう。BabelはまさにJS界でもっとも多く使われているメタプログラミングのプロダクトです。Babelはプラグインやその集合体であるプリセットを使ってソースコードを変換します。
じつはBabelのプラグインの作成はとても簡単です。
const { transform } = require('@babel/core')
const plugin = {
visitor: {
BinaryExpression: nodePath => {
nodePath.node.operator = '*'
}
}
}
const src = '1 + 2;'
const { code } = transform(src, { plugins: [plugin] })
console.log(code) // --> 1 * 2;
たった7行ですが、これはsrcに書かれた二項演算子をかけ算に変えてしまうイタズラをするBabelプラグインです。
Babelプラグインはこのようにとても簡単に作れますが、それはBabelエコシステムが手厚いサポートしてくれるおかげです。字句解析・構文解析だけではなく静的解析による変数のスコープ解析や、ある変数に再代入が発生しているかどうかなどなど、様々な解析をしてくれます。
Babelを初めとするツールはASTと呼ばれるツリー状のデータ構造を扱います。他の言語ではASTは大抵はコンパイラの内部表現に過ぎないもので、JavaScriptほど簡単に扱えるものではありません。
筆者が技術書典3で出した簡単JavaScript AST入門のサンプルコードをいくつか紹介します。
- https://github.com/erukiti/ast-book-sample/blob/master/chapter3/babel-plugin-di.js
- https://github.com/erukiti/ast-book-sample/blob/master/chapter3/test-di.js
Dependancy Injection をお手軽に実現したもので前者がプラグイン本体で、後者がそのプラグインを叩いた実例です。
- https://github.com/erukiti/ast-book-sample/blob/master/chapter4/easy-optimizer.js
- https://github.com/erukiti/ast-book-sample/blob/master/chapter4/optimizer.js
前者はBabelの持つ機能を使うだけのお手軽最適化で、後者は変数の静的解析を元にさらに最適化するというものです。
- https://github.com/erukiti/ast-book-sample/blob/master/chapter6/overload.js
- https://github.com/erukiti/ast-book-sample/blob/master/chapter6/fraction.js
overload.jsではfraction.jsを解析して、Fractionの二項演算をFractionクラスのメソッド呼び出しに変換しています。
ほかに筆者が作っているものとしてはerukiti/autodebuggerというプロダクトがあります。
関数・メソッドの呼び出しをトレースしておいて、エラーで落ちた時にトレース結果を表示してくれるというものです。
またconsole.logに行番号などの情報を付与して賑やかにしています。
動的にも静的にもできるメタプログラミング
babelの場合は、@babel/registerというツールを使えば、動的にBabelでトランスパイルもできます。静的にも動的にもメタプログラミングできます。主に開発時は動的にトランスパイルし、プロダクションバージョンは静的にトランスパイルという使われ方がされます。autodebuggerは@babel/registerと同じ技術を使って動的にrequireをhackしてトランスパイルしています。
ところが最近新しい概念が提唱されました。それがコーディングタイムコンパイルです。
コーディングタイムコンパイル
静的にトランスパイルする場合、いちいちnpm build
なりyarn build
なり叩くのは面倒ではないでしょうか?多くのビルドツールはwatchモードと呼ばれるソースコードの変更を検知してビルドする機能を持っています。これは言ってみればコーディング時にリアルタイム変換をかけているということです。
あかめさんの書いたさよならボイラープレート。s2sによる高速reduxアプリケーション構築 - Qiitaという記事がバズっていましたが、その中に
これを実現している仕組みをSource to Source(s2s)といいます。
ソースコードからソースコードへのコーディングタイムコンパイルです。
という形でコーディングタイムコンパイルという概念が言葉になりました。
s2sはソースコードの生成・変更などを検出してBabelプラグインなどを走らせる事ができるもので、ボイラープレートやコードの繰り返しを撲滅するためのプロダクトです。
コーディングタイムコンパイルを使ってフレームワークを作ることも十分可能です。じつはs2sのプラグインは極端な話ASTを触らなくても書けますし、JavaScript以外も扱えます。
- 静的メタプログラミングの利点である実行時の負荷軽減
- 動的メタプログラミングと同様のお手軽さがあり、開発支援を得られる
- IDE支援のためのコード生成もできるため、エディタやIDEに、フレームワーク用のプラグインを入れる必要が無い
Railsと同等、あるいはそれ以上にDRYで高性能なフレームワークを実現可能な概念がコーディングタイムコンパイルなのです。
課題
まだまだs2sの歴史自体が浅く、またJavaScript ASTに関しても知る人ぞ知るという普及具合です。
AST自体はそんなに難しいものでもありませんが難しいものだと思っている人が多いようです。
大きなポテンシャルがありますが多少面倒なところがあるのも確かで、もっと簡易的にメタプログラミングができるライブラリ・フレームワークがそろえば、プロダクトにカジュアルにs2sを導入して、重複を撲滅することもできるでしょう。
筆者もいろいろ模索中でerukiti/meta-programming-utilsというツールを作ってみたりしています。またs2sで独自のハンドラーを使って実際にアプリ開発の実験をしています。
これからの技術
s2sやコーディングタイムコンパイルを利用したDRYなフレームワークが登場すれば、JavaScriptプログラミングの世界は一気に変わるはずです。
そのときにはきっと真のUniversalが実現されているでしょう。
コーディングタイムコンパイルに限らず、メタプログラミングを最大限に活かせば、サーバー向けのコードもフロント向けのコードも、あるいはモバイル向けのコード(の一部)も生成できます。動的にそれを実現するプロダクトとしてはmeteorなどがありますが、もっと汎用的に実現できるはずです。
「あなたの手」で書くコードを最小限にする技術は今後メタプログラミングを活用して進化していくでしょう。カジュアルにメタプログラミングができるJavaScriptであれば古典的なメタプログラミングの制約も関係ありません。今まで囚われていた様々なしがらみをメタ視点で解決していきましょう。
最後に
プログラミングにまつわる邪悪を撲滅する為にメタプログラミングを活用することこそがこれからの時代に求められているものです。
- 簡単JavaScript AST入門 - 東京ラビットハウス - BOOTH で技術書典3で出したAST本の電子版を販売開始しました
JavaScript ASTは決して難しいものでは無いということを本書で知っていただきたいです。
異論反論大歓迎なので、是非コメントなりツイートなり頂ければと思います。