これからの時代のメタプログラミングJavaScriptの正義を語ろう
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は決して難しいものでは無いということを本書で知っていただきたいです。
こういう新しい技術って、いつの間にか消えて行っちゃうものも多いですが、見極めはどうされていますか?
技術の見極めってかなり奥深い問題なのできっと記事数本分くらいの話だと思うんですが、簡略化して僕なりの考えを言うと「戦略によるミスは戦術では取り返せない」「立場によって見極めに必要な要素がかわる」「嗅覚をみがく」です。
(1) 開発現場で採用する時の技術選定
今の開発シーンで当たり前とされてる技術をなるべく選択します。JavaScriptに限らず、ソース管理はgitを使う、githubのプルリク、コードフォーマッタ(JSならeslint, tslint, prettier)、ドキュメントは全て履歴管理のしやすい形式を使う、「選択理由」などの思想をドキュメントとして残す(もちろん履歴管理下にて)、CI(継続的インテグレーション)を最大限活用する、環境整備は極力人間の手を使わないようにする、デプロイも極力人間の手を使わないようにするなどです。
これらが実現できる環境でなければおそらく戦略的な問題か人材にまつわる問題を抱えています。技術的に解決するというよりは、政治、人事、組織体制、チームビルディングといった面の方が遙かに重要です。奇をてらった技術で解決しようとするよりは、大きな視点を持てる人を引き込むか自分がそうなるべきです。開発期間が半年以上かかるプロジェクトは足の遅いプロジェクトです。政治・組織などの要素がどうしても大きくなります。戦略のミスは戦術では取り返せないです。
実際にどういう技術を選ぶかですが、流行りの技術の最小公倍数を見極める、なるべく公式を読む、公式リポジトリ(ソースコード、ドキュメント、余力があればissueなど)を読むと、嗅覚が上がるかもしれません。
たとえばタスクランナーは使わない。なるべくpackage.jsonに単発コマンドを書いて
npm run build
やyarn build
で済むようにする。buildが複雑であればタスクランナーじゃなくて1つのjsファイルで行う(実際タスクランナーで書くのと難易度は変わりません)。できるだけシンプルにいける技術を選択すべきです。webpackはよく採用されていますが負債化しやすい面も大きいので、browserifyやrollupの検討もすべきでしょう。これらの最小公倍数は何かを考えるべきです。React, Vue, Angularなどが最近の流行りのライブラリ・フレームワークです。個人的にはこれらであれば、どれも適正(メンバーのスキルや好み)で選べばいいと思います。どれもある程度の未来があり、どれもある程度の廃れる可能性を持っています。
型に関してはFlowかTypeScriptですが、どちらを採用しても良い。Babel7ではTypeScriptも対応しているので移行ツールなんかも登場するのは間違いないですし。ある程度までは最小公倍数を意識した方がいいですが、どっちもできることは似たようなものです。
(2) この先生きのこるための考え方
情報工学の基礎だったり、設計に対する考え・経験値を最重視すべきです。つまりつぶしのきく部分が重視されます。ただしそればかりだと労力が大きすぎるので、手を抜けるところで手を抜くというのももちろん必要です。意識高い日々を毎日すごせると仮定してはいけません。自分のペースでゆっくりやれることが一番重要なことです。
「名前とは何か。この名前は適切か?この名前の付いた関数・クラス・変数は内容に見合ってるか?」を考え続ける事はとても大切です。正解があるとは限りません。考え続けることが大切です。
そういった大きな流れを踏まえた上で、たとえば1の冒頭に書いた今の開発シーンで当たり前とされてる技術の思想、それらを実現している技術についての知識を深めるのは得られる利益が大きいです。いくつアハ体験を積み重ねられるかで大きく変わっていきます。
開発現場での現実的な技術選定、開発者としての力を貯める、どちらもバランスよくできるのが最良です。技術にそこまで興味が持てない、ということであれば無理して持つ必要もありません。別の代わりになる力があれば良いだけです。技術ができる人に任せる、コミュニケーションを磨く、いい企画を作る、いいUI/UXを追求する、いろいろな生き残り方があります。
(3) エッジ
僕は技術の発展が大好きな人間なのでエッジを追いかけていますが、これはもう個々人の適正、相性、もっといえば好きか嫌いかです。
たとえば僕はs2sとメタプログラミングが示した可能性に最大限に惚れています。
長文のコメントありがとうございます。このコメント自体、内容が濃いので、もうちょい加筆して、ひとつの投稿にされたらいかがでしょう?
あと、自分の見方ですが、生き残る技術ってシンプルなものが多くないですか?
Angular2 なんて、自分から見れば複雑すぎて、オワコンの雰囲気なんですが。