15K 行のアプリを TypeScript 1.8 から 2.0 に移行してみた

先日 TypeScript の新しいメジャーバージョン 2.0 のコンパイラの beta 版がリリースされました.

コンパイラのチェックの強化や非 null 型,tagged union など,いくつかの機能が追加・強化され,自分の趣味プロジェクトでも恩恵に与れそうだったので試しに移行してみました.

移行してみたのは下記の Electron でつくり中の Twitter Client アプリ(React+Redux)で,全体で大体 15000 行ぐらいです.

github.com

下記の手順で修正してみました.

  1. コンパイラをアップデートしてビルドしてみる
  2. --noImplicitThis--noUnusedLocals, --noUnusedParameters を有効にしてみる
  3. --strictNullChecks を有効にして nullability をチェックするようにする
  4. include で glob を使う
  5. Tagged union types で Redux の Action types をリファクタリングする
  6. 引数リストに trailling comma を入れていく
  7. インターフェースやクラスメンバを readonly 指定する

各段階でビルド&単体テスト確認をするのが大事だと思うので,なるべく細かく段階を分けました.コミットも大体各作業ごとになっているので,随時 diff のリンクを貼ります.

「説明しなくて良いからコードを見せろ」という猛者向けに,下記が今回行った作業の全 diff です.+-1000行弱ぐらいですね.

diff

コンパイラをアップデートしてビルドしてみる

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 で導入された機能です.

基本的に煩雑さが減る方向のはずなので,修正は容易なはずです.

diff

--noImplicitThis--noUnusedLocals, --noUnusedParameters を有効にしてみる

2.0 ではいくつかのチェック機能がコンパイラに追加されました.互換性が理由(?)でデフォルトはオフのようですが,特に理由が無ければ有効にしておいたほうが良さそうです. tsconfig.json"noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true のように指定できます.

機能名の通り各チェックを行って,検知した時はコンパイルエラーに落としてくれます.特に noUnusedLocals は tslint で検知してくれなかった未使用の import を検出してくれて助かりました.

diff

ここまではたとえ 2.0 にすぐに上げられないプロジェクトでも一時的にコンパイラのバージョンを上げてチェックさせることで恩恵に与れそうです.

--strictNullChecks を有効にして nullability をチェックするようにする

2.0 では null の型がそのまま null となり,T | null のように書くことで nullable な型を表現できます.また,undefined についても同様に T | undefined のように書けます. さらに tsconfig.json"strictNullChecks": true を追加することで,デフォルトで全ての型を non nullable type として扱うようにできます.

基本的には

  1. null を代入しているのに nullable でない変数の型に | null を追加する
  2. null が来る可能性があるのにチェックしていない箇所は素直にチェックする処理を入れる
  3. 型は 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.tsStatelessComponent です.

interface StatelessComponent<Props> {
    (props?: Props, context?: any): ReactElement<any>;
    // ... その他雑多なプロパティ
}

上記の props? が問題です.

const MyComponent: React.StatelessComponent<Props> = props => ...;

今までは上記のようにコンポーネントの型を指定し,引数を推論させていてこれでうまくいっていました.ですが,引数は props? となぜかオプショナルに定義されているため, 2.0 からは props の型は Props | undefined に推論され,propsundefined でないかどうかのチェックを入れる必要が出てきてしまいます. (僕の知識が足りないだけで実は props?context? が実際に undefined になるようなケースがひょっとしたらあるのかもしれないですが…もしそうなら教えていただけるとうれしいです)

仕方がないので下記のようにしてワークアラウンドを入れています.

const MyComponent = (props: Props) => ...;

これだと引数の型はプログラマが指定したものになるので undefined チェックを回避できます.現状ではこれで問題になっていませんが,上記型定義の「その他雑多なプロパティ」がほしくなった場合は困ることになります.

また,react-redux.d.tsconnect() も困りました.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**/*.tsxtypings/*.d.ts のように指定すれば良くなりました.

diff

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 便利.

diff

引数リストに trailling comma を入れていく

個人的にかなり嬉しい機能なのですが,2.0 からは関数の引数に trailing comma が許されるようになりました(公式の例). (配列やオブジェクトの要素は前から trailing comma を許してます).

tslint がチェックしてくれるようになるのを待っても良いのですが,せっかくなので手で置き換えました.\h\w*\s*\(\n あたりで適当に grep で引っ掛けて修正します. Vim なら一度行末にコンマを追加する修正を入れれば,他の箇所は . で繰り返すだけで OK ですね.Vim 便利.

diff

インターフェースやクラスメンバを readonly 指定する

変更するつもりのないインターフェースのプロパティやクラスメンバに 2.0 から導入された readonly 指定をつけてまわります.基本的に大抵のプロパティは変更されないと思うので,デフォルトで readonly 付けるぐらいの勢いで良さそうです.:%s/^\s*\zs\ze\w\+?\=:/readonly /g とかやると雑にプロパティに readonly を付けられるので,付けてみてコンパイラに怒られたらそのフィールドを外す感じで機械的に作業できます.Vim 便利.

diff

まとめ

最近ブログ書いてなかった(ネタはあったのに)ので,久々に人柱になった内容をまとめてみました.まだ beta ではありますが,今のところ変なバグは踏んでません.--strictNullChecks 以外はとりあえずやっておいて損は無さそうです.せっかくなので,いずれ 2.0 が正式リリースされた時に役立つと良いなぁと思っています.

『SD別冊 Vim&Emacs』と『SoftwareDesign 5月号』に寄稿しました

SD別冊 Vim&Emacs と SoftwareDesign 5月号の Vim 特集にそれぞれ寄稿させていただきました.

SD別冊 VimEmacsエキスパート活用術

http://gihyo.jp/book/2016/978-4-7741-8007-6

www.amazon.co.jp

Software DesignVim 特集と Emacs 特集を集めて1冊の本にしたものです.VimEmacs について,入門から拡張の紹介,キーボードの話や昔話まであります.1冊読み終える頃にはエディタ成分を過剰多量に摂取できていることでしょう.

僕は 2015年 1月号に寄稿させていただいた「犬でも分かる!?Vim 導入&カスマイズ超基本」という記事を古くなっている箇所を改定して寄稿させていただきました.特にこれから Vim を使い始めたいと考えている方を対象に,インストール方法から Vim の入門の仕方(チュートリアルプラグイン導入,ヘルプの読み方など)を解説しています.

Software Design で「犬でも分かる!? Vim 導入&カスタマイズの超基本」という記事を書きました

全体を通して個人的に印象深かったのは,まつもとゆきひろさんの記事で EmacsGC 実装などを見て言語実装を学んだというところでした. 確かに EmacsLisp マシンとその上に Lisp で実装されたエディタという構成で,Atom にちょっと似ています(と言っても Atom は下地が Chromium なので全然厚さが違いますが).Emacs に次の GC 発動までに割り当てるメモリサイズとかの指定ができることを考えると普通のマーク・アンド・スイープなのかな.

Emacsruby-mode も実装したとのこと.Rubyend で終わる構文は地味に厄介で,ある endclass ブロックのものなのか if ブロックのものなのか…というのはぱっとみただけでは分かりません.雑にハイライトするだけなら大して問題にはならないのですが,class ... endif ... end では別のハイライト色を割り当てたいとかなるとちょっと考える必要があります.というのを vim-crystal を実装した時に考えたのを思い出しました.

Sofware Design 5月号

http://gihyo.jp/magazine/SD/archive/2016/201605

www.amazon.co.jp

VimGitHub の連携についての記事を『第1特集 コード編集の高速化からGitHub連携まで Vim[実戦]投入』に寄稿させていただきました.こちらは新規に書いた記事です.

仕事でも趣味でも GitHub を使った開発が非常に一般的になってきましたが,Vim はエディタ,Githubウェブサービス,Git は(主にコマンドラインベースの)VCS という都合上,どうしてもエディタとターミナル,ブラウザ間の移動が多くなってしまいがちです.そこで,Vim プラグインを使って VimGitHub の連携をスムーズにすることを目的とした記事を書きました.下記のような 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 が動くようです.

googledevjp.blogspot.jp

自作言語のコンパイラLLVM フロントエンドとしてつくっているので,これは試さないわけにはいきません.

github.com

というわけで,さっそく試してみます.

準備

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

binaryenLLVM IR などを WebAssembly 形式に落とすコンパイラです.適当に clone してきて cmakemake でビルドすると bin ディレクトリ内にいくつかのコマンドが生成されます.

4. Dachs

誰も試さないと思いますが一応… clone してきて cmakemake すると 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 名).

sqrt2.ll

さらに llc を使ってアセンブラ形式に落とします.

$ /usr/local/opt/llvm/bin/llc sqrt2.ll -march=wasm32
$ cat sqrt2.s

sqrt2.s

これで準備ができました.binaryen を使ってアセンブリを wasm32 形式にコンパイルします.まずは WebAssembly の AST をテキスト+S式で表現した wast 形式にコンパイルします.

$ s2wasm sqrt2.s > sqrt2.wast

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>

index.html

最後に Canary 版の Chrome で index.html を開いてみます.

f:id:rhysd:20160324235304p:plain

画像内の DevTools のコンソールにあるように,Wasm.instantiateModule を使って JavaScript で実行できるモジュールに変換できます.今回はニュートン法平方根が期待通り計算できていることが分かりました.

まとめ

自作言語がブラウザ上で動いているのはちょうたのしい.

  • 追記

勢い余って Vim の wast filetype 対応プラグインつくりました.

github.com