15K 行のアプリを TypeScript 1.8 から 2.0 に移行してみた
先日 TypeScript の新しいメジャーバージョン 2.0 のコンパイラの beta 版がリリースされました.
コンパイラのチェックの強化や非 null 型,tagged union など,いくつかの機能が追加・強化され,自分の趣味プロジェクトでも恩恵に与れそうだったので試しに移行してみました.
移行してみたのは下記の Electron でつくり中の Twitter Client アプリ(React+Redux)で,全体で大体 15000 行ぐらいです.
下記の手順で修正してみました.
- コンパイラをアップデートしてビルドしてみる
--noImplicitThis
,--noUnusedLocals
,--noUnusedParameters
を有効にしてみる--strictNullChecks
を有効にして nullability をチェックするようにするinclude
で glob を使う- Tagged union types で Redux の Action types をリファクタリングする
- 引数リストに trailling comma を入れていく
- インターフェースやクラスメンバを
readonly
指定する
各段階でビルド&単体テスト確認をするのが大事だと思うので,なるべく細かく段階を分けました.コミットも大体各作業ごとになっているので,随時 diff のリンクを貼ります.
「説明しなくて良いからコードを見せろ」という猛者向けに,下記が今回行った作業の全 diff です.+-1000行弱ぐらいですね.
コンパイラをアップデートしてビルドしてみる
TypeScript 2.0 beta は下記のコマンドで入るので,サッと入れてとりあえず npm run build
してみます.
$ npm install --save-dev typescript@beta
TypeScript 2.0 では処理フローを見て型を付けるようになったので,この時点でビルドが失敗することがあります. 例えば下記みたいな場合です.
type T = 'aaa' | 'bbb'; const v: T = 'aaa'; switch (v) { case 'aaa': /* ここでは v の型は 'aaa' */ console.log('OK'); break; case 'bbb': /* ここでは v の型は 'bbb' */ console.log('OK'); break; default: /* ここでは v の型は never */ console.log('NOT OK', v); break; }
このように,制御フローを見て適宜型を切り替えてくれるようになりました.
上記のように v
の型として取りうる可能性のある型がなくなると never
という型になり,v
を参照しようとするとエラーになります.
never type も 2.0 で導入された機能です.
基本的に煩雑さが減る方向のはずなので,修正は容易なはずです.
--noImplicitThis
,--noUnusedLocals
, --noUnusedParameters
を有効にしてみる
2.0 ではいくつかのチェック機能がコンパイラに追加されました.互換性が理由(?)でデフォルトはオフのようですが,特に理由が無ければ有効にしておいたほうが良さそうです.
tsconfig.json で "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true
のように指定できます.
機能名の通り各チェックを行って,検知した時はコンパイルエラーに落としてくれます.特に noUnusedLocals
は tslint で検知してくれなかった未使用の import
を検出してくれて助かりました.
ここまではたとえ 2.0 にすぐに上げられないプロジェクトでも一時的にコンパイラのバージョンを上げてチェックさせることで恩恵に与れそうです.
--strictNullChecks
を有効にして nullability をチェックするようにする
2.0 では null
の型がそのまま null
となり,T | null
のように書くことで nullable な型を表現できます.また,undefined
についても同様に T | undefined
のように書けます.
さらに tsconfig.json に "strictNullChecks": true
を追加することで,デフォルトで全ての型を non nullable type として扱うようにできます.
基本的には
null
を代入しているのに nullable でない変数の型に| null
を追加するnull
が来る可能性があるのにチェックしていない箇所は素直にチェックする処理を入れる- 型は nullable だけど実質
null
な値が来ないところには null assertion 演算子(後置!
)を使う
という処理を入れていけば修正は OK です.3. は例えば下記のような感じです.
// 対象の要素が無い時 getElementById は null を返すので戻り値型は本来 // Element | null だが,静的に HTML に id が振られているので実質失敗しない // expr! で expr が null にならないとコンパイラに伝える const elem = document.getElementById('root')!;
また,null assertion 演算子は undefined
にも使えます.
変更箇所もなかなか多かったのですが,他にも細々とうまくいかないところがあり,この変更は思ったより大変でした.
例えば,strictNullChecks
によってオプショナル引数の型が以前と変わります.
function foo(optional?: number) { // 1.8 まで -> number // 2.0 から -> number | undefined optional; }
これだけ見ると問題ないように見えますが,型定義ファイルには,実質オプショナルでないのにオプショナル引数になっている定義が割といっぱいあります. 特にコールバックの引数の指定がまずいものが多く,1.8 までは型的には変わらなかったので問題にならなかったのですが 2.0 では問題になります.
今回例えば困ったのは react.d.ts
の StatelessComponent
です.
interface StatelessComponent<Props> { (props?: Props, context?: any): ReactElement<any>; // ... その他雑多なプロパティ }
上記の props?
が問題です.
const MyComponent: React.StatelessComponent<Props> = props => ...;
今までは上記のようにコンポーネントの型を指定し,引数を推論させていてこれでうまくいっていました.ですが,引数は props?
となぜかオプショナルに定義されているため,
2.0 からは props
の型は Props | undefined
に推論され,props
が undefined
でないかどうかのチェックを入れる必要が出てきてしまいます.
(僕の知識が足りないだけで実は props?
や context?
が実際に undefined
になるようなケースがひょっとしたらあるのかもしれないですが…もしそうなら教えていただけるとうれしいです)
仕方がないので下記のようにしてワークアラウンドを入れています.
const MyComponent = (props: Props) => ...;
これだと引数の型はプログラマが指定したものになるので undefined
チェックを回避できます.現状ではこれで問題になっていませんが,上記型定義の「その他雑多なプロパティ」がほしくなった場合は困ることになります.
また,react-redux.d.ts
の connect()
も困りました.connect()
は mapDispatchToProps
(第2引数)だけを指定したいときは第1引数を null
にできるのですが,第1引数の型定義に | null
が入っていないため
null
を渡せません.仕方ないので手元で react-redux.d.ts
に手を入れ,リポジトリに含めて typeings でインストールしたものの代わりにそちらを見るようにして凌いでいます.
型定義ファイルの更新状況などを見ながら,strictNullChecks
を付けるのは少し待ったほうが良いかもしれません.DefinitelyTyped では今のところ TypeScript 2.0 向けの型定義ファイルの開発を types-2.0 ブランチで進めているようです.
include
で glob を使う
TypeScript のプロジェクトファイル tsconfig.json で指定するソースコード一覧には今まで glob が使えなかったためファイルを1つずつ追加していました.
2.0 からは晴れて *
や **
が使えるようになったのでそれを使うようにします.
**/*.ts
や **/*.tsx
,typings/*.d.ts
のように指定すれば良くなりました.
Tagged union types で Redux の Action types をリファクタリングする
2.0 では tagged union types という機能が入り,union types のどの型の値が入っているのかを,各型に共通のプロパティで判断してくれます. プロパティには string literal type を指定すれば良いようです.公式の What's New に載っている例が分かりやすいです.
flux や redux では type
プロパティを見てアクションの種類を判断するため,アクションの型を tagged union type にすることでうまく型がつくようになります.
/* * action_type.ts */ // 'type' プロパティがタグになる type Action = { type: 'AddSomething', item: Something, } | { type: 'Sort', kind: Kind, } | { type: 'DeleteLast', }; export default Action; /* * actions.ts */ import Action from './action_type'; export function addSomething(item: SomeThing): Action { return { type: 'AddSomething', item, }; } export function sort(kind: Kind): Action { return { type: 'Sort', kind, }; } export function deleteLast(): Action { return { type: 'DeleteLast', }; } /* * reducer.ts */ import Action from './action_type'; function reduce(state: State = DefaultState, action: Action) { switch (action.type) { case 'AddSomething': // ここでは action の型は {type: 'AddSomething', item: Something} になる const next = ...; return next; case 'Sort': // ここでは action の型は {type: 'Sort', kind: Kind} になる const next = ...; return next; case 'DeleteLast': // ここでは action の型は {type: 'DeleteLast'} になる const next = ...; return next; default: return state; } }
大体こんな感じです.これで各アクションの処理内で間違ったプロパティにアクセスしていた時などは全てコンパイルエラーになります. 文字列リテラルが複数箇所にあるのが気になるかもしれませんが,全て string literal 型としてコンパイラがチェックするのでタイポしていてもコンパイルエラーになり安心です. 良い感じです.
ちなみに以前は type
プロパティが取る値を全部 Kind
というオブジェクトに入れて Kind.AddSomething
のように参照していたので,:%s/Kind\.\(\w\+\)/'\1'/g
のように置換して少しマクロを使ってやるだけで
かなり簡単に置き換えられました.Vim 便利.
引数リストに trailling comma を入れていく
個人的にかなり嬉しい機能なのですが,2.0 からは関数の引数に trailing comma が許されるようになりました(公式の例). (配列やオブジェクトの要素は前から trailing comma を許してます).
tslint がチェックしてくれるようになるのを待っても良いのですが,せっかくなので手で置き換えました.\h\w*\s*\(\n
あたりで適当に grep で引っ掛けて修正します.
Vim なら一度行末にコンマを追加する修正を入れれば,他の箇所は .
で繰り返すだけで OK ですね.Vim 便利.
インターフェースやクラスメンバを readonly
指定する
変更するつもりのないインターフェースのプロパティやクラスメンバに 2.0 から導入された readonly
指定をつけてまわります.基本的に大抵のプロパティは変更されないと思うので,デフォルトで readonly
付けるぐらいの勢いで良さそうです.:%s/^\s*\zs\ze\w\+?\=:/readonly /g
とかやると雑にプロパティに readonly
を付けられるので,付けてみてコンパイラに怒られたらそのフィールドを外す感じで機械的に作業できます.Vim 便利.
まとめ
最近ブログ書いてなかった(ネタはあったのに)ので,久々に人柱になった内容をまとめてみました.まだ beta ではありますが,今のところ変なバグは踏んでません.--strictNullChecks
以外はとりあえずやっておいて損は無さそうです.せっかくなので,いずれ 2.0 が正式リリースされた時に役立つと良いなぁと思っています.
『SD別冊 Vim&Emacs』と『SoftwareDesign 5月号』に寄稿しました
SD別冊 Vim&Emacs と SoftwareDesign 5月号の Vim 特集にそれぞれ寄稿させていただきました.
SD別冊 Vim&Emacsエキスパート活用術
http://gihyo.jp/book/2016/978-4-7741-8007-6
Software Design の Vim 特集と Emacs 特集を集めて1冊の本にしたものです.Vim と Emacs について,入門から拡張の紹介,キーボードの話や昔話まであります.1冊読み終える頃にはエディタ成分を過剰多量に摂取できていることでしょう.
僕は 2015年 1月号に寄稿させていただいた「犬でも分かる!?Vim 導入&カスマイズ超基本」という記事を古くなっている箇所を改定して寄稿させていただきました.特にこれから Vim を使い始めたいと考えている方を対象に,インストール方法から Vim の入門の仕方(チュートリアル,プラグイン導入,ヘルプの読み方など)を解説しています.
Software Design で「犬でも分かる!? Vim 導入&カスタマイズの超基本」という記事を書きました
全体を通して個人的に印象深かったのは,まつもとゆきひろさんの記事で Emacs の GC 実装などを見て言語実装を学んだというところでした. 確かに Emacs は Lisp マシンとその上に Lisp で実装されたエディタという構成で,Atom にちょっと似ています(と言っても Atom は下地が Chromium なので全然厚さが違いますが).Emacs に次の GC 発動までに割り当てるメモリサイズとかの指定ができることを考えると普通のマーク・アンド・スイープなのかな.
Emacs の ruby-mode も実装したとのこと.Ruby の end
で終わる構文は地味に厄介で,ある end
が class
ブロックのものなのか if
ブロックのものなのか…というのはぱっとみただけでは分かりません.雑にハイライトするだけなら大して問題にはならないのですが,class ... end
と if ... end
では別のハイライト色を割り当てたいとかなるとちょっと考える必要があります.というのを vim-crystal を実装した時に考えたのを思い出しました.
Sofware Design 5月号
http://gihyo.jp/magazine/SD/archive/2016/201605
Vim と GitHub の連携についての記事を『第1特集 コード編集の高速化からGitHub連携まで Vim[実戦]投入』に寄稿させていただきました.こちらは新規に書いた記事です.
仕事でも趣味でも GitHub を使った開発が非常に一般的になってきましたが,Vim はエディタ,Github はウェブサービス,Git は(主にコマンドラインベースの)VCS という都合上,どうしてもエディタとターミナル,ブラウザ間の移動が多くなってしまいがちです.そこで,Vim プラグインを使って Vim と GitHub の連携をスムーズにすることを目的とした記事を書きました.下記のような tips 形式で紹介しています.
- Vim から Git を使うための Vim 本体の設定
- Vim からコミットを見る(コミットブラウザ)
- Vim から GitHub をブラウザでシュッと開く
- Vim 内でイシュー確認
- GitHub ではほぼ必須な Markdown ドキュメントの効率的な編集(表記法とか)
- リポジトリ URL や issue 番号,絵文字の補完
- Vim + Gist 連携
Git や GitHub 使ったこと無いよ!という方もいると思いますので,どうやって学べば良いかも introduction として書いてみました.
また,他の記事に目をやってみると,
- ujihisa さんの入門記事.インストールからプラグインの概要まで幅広い内容です.
- thinca さんの中級者向け機能紹介.矩形選択やオペレータ・テキストオブジェクト,ドットによるリピート,
gn
など「とりあえず Vim の基本操作は分かった」な人が読むとかなり勉強になる内容です. - tyru さんの正規表現解説.Vim の若干とっつきにくく強力な正規表現の解説です.ここまでしっかり Vim 正規表現とその使いみちについてまとまっている内容はなかなか無いですね.
- mattn さんの Vim の歴史と最新の開発事情について.今年に入ってから version 8 に向けて非常に活発に開発が進んでいる Vim の開発事情と新機能がとても興味深いです.
といった感じで,Vim を使い始めたい初心者,もっと使いこなしたい中級者,Vim ジャンキー各位どの層にもうれしいラインナップとなっています.発売は明日(4/18)らしいので,是非手にとってほしいです.
なお,他の特集については Ubuntu 16.04 LTS の記事とかがとても参考になりました.Python3 や systemd,日本語入力周りの更新,LXD など色々あるもよう.
まとめ
この時期に Vim 関連の特集および別冊を立て続けに出してくるのは技術評論社なかなか攻めてきてるなという印象でした.エディタはプログラミングする上で欠かせないので,春から新しくプログラミングを始めた人とかにリーチすると良いなぁと思っています. 最近は組み込みな現場を離れてウェブサービス開発する部署にいるんですが,周りを見ると Atom 使ってる人が多いですね.Atom とか VS Code の特集も読みたい.
校正などでアドバイスをいただいた編集の方々および vim-jp の方々ありがとうございました.
WebAssembly を使って自作言語をブラウザで動かしてみよう
今日 Google の開発者ブログで WebAssembly の記事が載っていました.どうやら最新の Chrome では WebAssembly が動くようです.
自作言語のコンパイラを LLVM フロントエンドとしてつくっているので,これは試さないわけにはいきません.
というわけで,さっそく試してみます.
準備
1. Chrome
直接 V8 をビルドするのは億劫なので Chrome のバイナリを落としてきて使います.Chrome 51.0.2677.0 以降であれば OK です.Canary 版をダウンロードしてきてインストールします.
次に chrome:flags
にアクセスして WebAssembly を有効にします.
2. LLVM
WebAssembly のためのアセンブリを吐くには LLVM の experimental な WebAssembly backend を有効にしてビルドする必要があります.Homebrew のフォーミュラをいじって使います.
$ brew edit llvm # ここで cmake の引数に -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly を追加 $ brew install llvm --HEAD
3. binaryen
binaryen は LLVM IR などを WebAssembly 形式に落とすコンパイラです.適当に clone してきて cmake
→ make
でビルドすると bin
ディレクトリ内にいくつかのコマンドが生成されます.
4. Dachs
誰も試さないと思いますが一応… clone してきて cmake
→ make
すると dachs
というバイナリができているはずです.
いざ試してみる
Dachs は Boehm GC を使って配列を割り当てています.本当は BrainFxxk のサンプルを動かしたいなぁと思っていたのですが,バッファを static に取るとうまく動かなくなったので諦めました.
今回はシンプルな sqrt2.dcs を使います.
func abs(n) ret if n > 0.0 then n else -n end end func sqrt'(p, z, x) ret z if abs(p-z) < 0.00001 ret sqrt'(z, z-(z*z-x)/(2.0*z), x) end func sqrt(x) ret sqrt'(0.0, x, x) end func main print(sqrt(10.0)) end
まずはコンパイルします.アセンブリをそのまま吐くオプションは無いので,まずは LLVM IR にコンパイルします.
$ dachs sqrt2.dcs --emit-llvm > sqrt2.ll
ここで少し sqrt2.ll を修正します.
- Dachs は単体で動作する汎用言語のため
main
関数がある前提ですが,WebAssembly は LLVM IR の module をコンパイルして生成するため不要な エントリポイントであるmain
関数を削除します. - mangling をサボっているので Dachs のコンパイラが吐く LLVM IR の関数名がひどくそのままでは使えないので少し関数名を修正します(C と同じ mangle 名).
さらに llc
を使ってアセンブラ形式に落とします.
$ /usr/local/opt/llvm/bin/llc sqrt2.ll -march=wasm32 $ cat sqrt2.s
これで準備ができました.binaryen を使ってアセンブリを wasm32 形式にコンパイルします.まずは WebAssembly の AST をテキスト+S式で表現した wast 形式にコンパイルします.
$ s2wasm sqrt2.s > sqrt2.wast
中を見てみるとS式形式に変換された LLVM IR みたいなものが吐き出されているのが分かります.
(module (memory 1) (export "memory" memory) (export "abs" $abs) (export "sqrt2" $sqrt2) (export "sqrt" $sqrt) (func $abs (param $$0 f64) (result f64) (block $label$0 (br_if $label$0 (i32.or (f64.gt (get_local $$0) (f64.const 0) ) (f64.ne (get_local $$0) (get_local $$0) ;; ...以下略
次にこのS式をシリアライズしたバイナリフォーマット wasm に変換します.ここで最初は llvm-as
を使ってコンパイルしてみたのですがうまくいきませんでした.ちょっと探してみると先人の知恵があったので,sexpr-wasm
を使うとうまくいくと分かりました.
$ sexpr-wasm sqrt2.wast -o sqrt2.wasm
0000000: 0061 736d 0a00 0000 140a 7369 676e 6174 .asm......signat 0000010: 7572 6573 0201 0404 0304 0404 0418 1366 ures...........f 0000020: 756e 6374 696f 6e5f 7369 676e 6174 7572 unction_signatur 0000030: 6573 0300 0100 0a06 6d65 6d6f 7279 0101 es......memory.. 0000040: 0120 0c65 7870 6f72 745f 7461 626c 6503 . .export_table. 0000050: 0003 6162 7301 0573 7172 7432 0204 7371 ..abs..sqrt2..sq 0000060: 7274 7e0f 6675 6e63 7469 6f6e 5f62 6f64 rt~.function_bod 0000070: 6965 7303 2000 0102 0700 0048 9b0e 000c ies. ......H.... 0000080: 0000 0000 0000 0000 980e 000e 000f 0090 ................ 0000090: 0e00 140e 0039 0001 0207 0000 9c12 008a .....9.......... 00000a0: 0e00 0e01 0cf1 68e3 88b5 f8e4 3e14 0e01 ......h.....>... 00000b0: 1412 010e 0189 0e01 8c8a 8b0e 010e 010e ................ 00000c0: 028b 0e01 0c00 0000 0000 0000 c00e 0211 ................ 00000d0: 0014 1201 0c00 0000 0000 0000 000e 000e ................ 00000e0: 00
あとはこれを適当に Uint8Array
に突っ込む JavaScript のコードを書き,HTML ファイルを書きます.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" /> </head> <body> <div id="test"></div> </body> <script> const buf = new Uint8Array([0x00, 0x61, 0x73, 0x6d, ...]); const module = window.Wasm.instantiateModule(buf); console.log(module); const result = module.exports.sqrt(314); document.getElementById('test').innerText = `Result: ${result}`; </script> </html>
最後に Canary 版の Chrome で index.html を開いてみます.
画像内の DevTools のコンソールにあるように,Wasm.instantiateModule
を使って JavaScript で実行できるモジュールに変換できます.今回はニュートン法で平方根が期待通り計算できていることが分かりました.
まとめ
自作言語がブラウザ上で動いているのはちょうたのしい.
- 追記