rougeのlexer開発ワークフロー

github.com

digdag の構文を追加するにあたって、Ruby製syntax highlighterであるところのRougeのlexerの開発方法を調べました。

基本

Rouge lexerはDSLで定義します: https://github.com/jneen/rouge#using-the-lexer-dsl

lexerはステートマシンとして振る舞うので、ただのキーワードマッチより遥かに強力で文脈を考慮した字句解析を行えます。

tokenの種類は token.rb に定義されているので、実際の出力はこれを見ながらふさわしいtoken typeを当てていくことになります。

また、lexerは継承もできます。たとえばdigdagファイルの構文はYAMLとほぼ互換性があるので、YAML lexerを継承することにしました。

そのなかで、カスタマイズしたいstateに prependappend でルールを追加することでlexerの拡張ができます。

例: https://github.com/jneen/rouge/pull/674/files#diff-801af56a286b5cd63a0e58c8cdad9867R64

テスト実行

lexerについて試行錯誤するときは、bin/rougify highlight でコンソールでハイライトを見れます。

たとえば今回であれば、以下のコマンドで確認しつつ開発をすすめました。

bundle exec bin/rougify highlight -m application/x-digdag -L debug=0 lib/rouge/demos/digdag

結果:

f:id:gfx:20170603153140p:plain

デバッグ実行

-L debug=1 を渡すと、デバッグモードで実行できます。ステートの遷移やどのルールにマッチしてどういうtokenを生成したかという情報を見れます。

digdag run (local mode) で並列実行数を制御する

とある分散バッチシステムでdigdagを導入してみています。

www.digdag.io

スケジューラ機能などは使っておらず、タスクを良い感じに並列実行するためのmakeよりちょっと便利なツール、くらいの感じで使ってます。

digdagは下記のようにloopなどを _parallel: true を指定するだけで並列実行できるのがいいですね。

timezone: UTC

+repeat:
  loop>: 3
  _parallel: true
  _do:
    +run:
      sh>: "echo ${i}"

+teardown:
  echo>: finish ${session_time}

ところで、このこの並列実行数を制御する方法がわからなかったのでコマンドラインオプションで渡せるようにpull-requestしました。 digdag v0.9.13 からたぶん使えます。

add --max-task-threads to `digdag run` by gfx · Pull Request #572 · treasure-data/digdag

digdag run --max-task-threads N foo.dig などとしてつかえます。まあ、このPRを出したあとに -X agent.max-task-threads=N でも指定できるとわかったのでちょっと冗長ですが、他のサブコマンド(server, scheduler)との一貫性もあるので存在意義はあるでしょう。

max-task-threadsの数によってloop taskの全体の結果はかわりません。たとえば --max-task-threads 1 のときにloopの最初のtaskが失敗すると、loopの他のタスクがすべて実行されて、loop taskそれ自体は失敗します。

検証コード:

timezone: UTC

+repeat:
  loop>: 3
  _parallel: true
  _do:
    +run:
      sh>: "if [ ${i} == 1 ] ; then false; else echo ${i}; fi"

+teardown:
  echo>: finish ${session_time}

結果:

$ digdag run --rerun mydag.dig
2017-05-28 10:35:31 +0900: Digdag v0.9.10
2017-05-28 10:35:32 +0900 [WARN] (main): Reusing the last session time 2017-05-28T01:00:00+00:00.
2017-05-28 10:35:32 +0900 [INFO] (main): Using session /Users/gfx/repo/mydag/.digdag/status/20170528T010000+0000.
2017-05-28 10:35:32 +0900 [INFO] (main): Starting a new session project id=1 workflow name=mydag session_time=2017-05-28T01:00:00+00:00
2017-05-28 10:35:33 +0900 [INFO] (0017@[0:default]+mydag+repeat): loop>: 3
2017-05-28 10:35:34 +0900 [INFO] (0017@[0:default]+mydag+repeat^sub+loop-0+run): sh>: if [ 0 == 1 ] ; then false; else echo 0; fi
2017-05-28 10:35:34 +0900 [INFO] (0020@[0:default]+mydag+repeat^sub+loop-1+run): sh>: if [ 1 == 1 ] ; then false; else echo 1; fi
2017-05-28 10:35:34 +0900 [ERROR] (0020@[0:default]+mydag+repeat^sub+loop-1+run): Task failed with unexpected error: Command failed with code 1
(snip)
0
2017-05-28 10:35:34 +0900 [INFO] (0021@[0:default]+mydag+repeat^sub+loop-2+run): sh>: if [ 2 == 1 ] ; then false; else echo 2; fi
2
2017-05-28 10:35:35 +0900 [INFO] (0021@[0:default]+mydag^failure-alert): type: notify
error:
  * +mydag+repeat^sub+loop-1+run:
    Command failed with code 1 (runtime)

なので、 max-task-threads の数は純粋にリソースの都合で調整すればよい、ということになります。

なぜTypeScript推しなのか

KibelaのフロントエンドをES2015からTypeScriptに絶賛移行中です。

www.typescriptlang.org

で、なぜ flow じゃないくてTSなのかって話です。

flow vs typescriptである理由は、どちらもJSのスーパーセットをうたう静的型付きのaltJSだからです。この時代にあえてaltJSを導入する理由としては静的型があるというのが必須で、かつ学習コストを考えるとJSのスーパーセットであるのが望ましいでしょう。

言語仕様

言語仕様の点から言うと、決定的な差はないと思っています。

メリットもだいたい同じで

  • 生産性: エディタの補完をJSよりも賢くできるので、より少ない脳のワーキングメモリでコードを書ける
  • 堅牢性: コンパイル時に(=多くのケースではエディタで)typoなどの間違いを検出できるのでバグを減らせる
  • 学習コスト: JSをベースにしており、実行もほぼJSなのでほぼモジュールシステムと型システムだけを学べばよい

検索しやすさ・コミュニケーションのしやすさ

検索しやすさは圧倒的にTypeScriptです。

flowは検索しにくいのはもちろんのこと、その名前の元になったと思われるflow analysis との混同が激しく、flowやTypeScriptの文脈で口頭で「flow解析が云々」といってもだいたいflowのことだと勘違いされて通じません。

ファイルタイプに対する誠実さ

flowの拡張子は .js、TypeScriptの拡張子は .ts です。

拡張子でファイルタイプを判断することの多いこの世界線においては、flowがJavaScriptと同じ拡張子で互換性のない言語として書かせるのは、ファイルタイプに対する誠実さに欠けているのではないかと思っています。

もちろんマーケティング的に「flowは言語ではない。flowはJavaScriptである」と主張することは、心理的な敷居を下げるという意味では一定の効果があるかもしれません。しかし、その辺にあるコードをJSだと思って眺めているとコードハイライトエンジンやエディタが構文エラーを指摘してくる、というのは気持ちのいい体験ではありません。

開発言語

flowはOCamlです。

TypeScriptはTypeScriptです(つまりセルフホスティング)。

TypeScript処理系をJS(あるいはTS)からライブラリとして使用できるというのは大きなメリットです。

まとめ

以上です。言語としては決定的な差はないものの、それをとりまく環境はTypeScriptのほうが好ましいと考えています。ゆえに私はTypeScriptを推します。

Herokuのreview-appsが "pr-predestroy" をサポートして外部リソースの掃除をできるようになっていた

devcenter.heroku.com

Herokuのreview-appsはたとえHerokuを使っていなくても非常に便利なものですが、PR削除時にS3やElasticsearchなど外部にホストしているリソースを掃除する方法がありませんでした。

ところが、最近は pr-predestroy hookが実装されたようで、外部リソースの掃除ができるようになったみたいですね。

ますます便利になりありがたい!

mitamaeでrecipeのロードエラー時にデバッグしやすくした

一部のプロジェクトでmitamae (itamae on mruby) を使ってるんですが、自分が書いているときはともかく他人が書いているmiamaeでrecipeのロードエラーが発生すると、mruby-ioレベルでもmitamaeレベルでもファイル名を出力してくれなくてこれはデバッグできないぞという状態でした。

問題は2つのレイヤーであって、

  • mruby-io がopenの失敗時にファイル名を出力しない
  • mitamaeのロガーは読み込んだレシピを実行するときはログを吐くが、ロードの前あるいは失敗時の処理がない

低レイヤーのmruby-ioはopenは失敗時にファイ名名を出力すべきだし、miamaeレイヤーでもレシピの読み込みは非常に重要なのでもっと詳細にログに出してほしいところです。なおmitamaeレベルでのレシピ読み込みエラーはopen由来とも限らないので、どちらか一方ではなく両方のレイヤーで対応するのが一番いいと思っています。

というわけで両方なおして mitamae v1.4.5 にしてもらいました。

See Also

AndroidにおけるJava8のサポート状況 2017年版

公式ドキュメントにありました。一言でまとめると、Android Oのpreviewが出た現在においても「Android N (API version 24)と同水準」となっています。

Use Java 8 language features | Android Studio

Android Studio 2.4 preview 4 (およびそれが要求するツールチェイン)の段階では、 desugar と呼ばれるツール(実体はAndroid Gradle PluginのTransform APIによるbytecode weaving tool)によって、一度javacでコンパイルしたバイトコードのJava8の言語機能(lambda, repating annotationsなど)をJava6水準のバイトコードに変換し、それをdxコマンドでdexにコンパイルするというプロセスを経るようです。このdesugarされたバイトコードは $subproject/build/intermediates/transforms/desugar/* にクラスファイルとして残されるので、どのようにバイトコードが変更されたかは確認できます。

この変換されたバイトコードをみるかぎり、Android O相当のAndroid SDKによってサポートされた「Java8の言語機能」はretrolambdaとほぼ同じです。

件のドキュメントの表にもあるとおり、 “Java8 Language API” つまりJava8の標準ライブラリ(stream, optional, etc.)を使うには依然としてAPI level 24が必要で、状況としては一年前のAndorid Nの時点とほぼ変わりません。

つまり、Android Oのpreviewが出ている現在でも、Java8に関して去年からの唯一のアップデートは、 retrolambda相当のツールがAndroid SDKに同梱されるようになった というだけということになります *1

Android Oで java.lang.invoke パッケージが追加されていたので、minSdkVersion = O ならもしかしたらinvokedynamicを使った本物のlambdaを使えるのではないかと思ったのですが、いまのところそのようにコンパイルされることは確認できませんでした。つまり、minSdkVersionの値に関わらず、desugarが必ず入るようです。このあたりはもしかしたら正規版までに変更があるかもしれませんが。

*1:去年の時点でβだったJackは正規版になるまえにdeprecatedになったのでノーカンで。

React Reduxファーストインプレッション

今更感ありますがReact Reduxを導入したの所感をメモしておきます。

github.com

  • ざっとみてこれなら自分でも再実装できそうだなという印象
  • いままではreact-micro-container でfluxしてた
  • React Reduxにすると、個々のreact componentをfluxフレームワークに依存しない形で設計できる
    • これに対して react-micro-container はcontainerに制御されることを意識した設計になる(=containerとcomponentで設計が異なる)
    • React ReduxなしでReactを初めてRect Reduxを導入するのは簡単だし、あとから別のflux実装にするのも簡単にできそう
    • アプリケーションに小さく導入することもそこまで難しくない
  • 個々のcomponentはシンプルに設計できていいが、Reduxとの繋ぎ込みでReact Reduxが魔法のように処理するところがやや多く、かつ繋ぎこみ用のマッピングが冗長な印象はある
  • 型がない素のJSだと繋ぎこみ部分がかなり不安。flowtypeなりTypeScriptなりは導入しないといずれ破綻しそう
  • ディレクトリ構成に悩む。まだ考え中

『Androidを支える技術』(I, II) の内容に興奮した

  • 『Androidを支える技術 I』 ~ 60fpsを達成するモダンなGUIシステム ~
  • 『Androidを支える技術 II』 ~ 真のマルチタスクに挑んだモバイルOSの心臓部 ~

これらを著者の有野さん よりご恵贈いただきました。ありがとうございます。

始めて知る内容も多かったのですが、既に知っていることでも著者の意見が反映されているのを読むと、いくつものモバイルOSを見てきたハッカーからみるとこう見えるのか!という新鮮な面白さがありました。

IとIIのテーマは独立しているので、どちらから読んでもいいと思います。

以下個人的に面白かった章をピックアップします。

I の見どころ

  • §1: ActivityThread.java にあるAndroidアプリのエントリポイント public static void main(String[] args) の役割
    • ActivityTheadはデバッグしてるとたまに見ることはあるものの全然役割を知らず、それどころかここに main() があるのすら知りませんでした
  • §3: Thread, Handler, Looperあたりの話
    • HandlerとLooperまわりは難解で、なんとなく表面的に知っているだけにとどまっていました。そのあたりを詳しく解説しているのは大変ありがたい…
  • §7: バイトコード実行系の話
    • Android 7.0でARTからVM + on-demand compileに戻ったのは初めて知った!たしかにART時代のAOT compileの遅さは気になっていたので、AOSPチームのその判断自体は納得できます

IIの見どころ

  • §3: OOM Killerとの戦いは涙なしには読めませんね…
  • §4: IIで一番驚いたはなんといってもinstance stateの保存(Bundle)に関して、「BundleがSystemServerのメモリ上に保持されている」という点です。そんな事がありえるのかと思うのですが、調べたかぎりだとそうとしか思えないとのこと
  • 本作はコラムが本体かってくらいの力作で、特にモバイルOSの歴史編(勝手に命名)はAndroid, iOS以前のモバイルOSの歴史を全然知らない私にとっては黎明期の壮大な物語です。こういうのが読みたかった。

なお『Androidを支える技術』レビュアーでありGoogleエンジニアでもあるomoさんのエッセイもすばらしいです。

死んでしまったOSたちへ – To Phantasien

MathJaxによる描画を細かく制御する

MathJax というのはLaTeXなどで書いた数式を美しく描画するための処理系です。 なおこのエントリは MathJax v2.7.0 を対象にしています。

www.mathjax.org

まず前提として、ほとんどのケースではMathJaxをconfigパラメータを与えてロードするだけで自動的にtypeset *1 が行われます。

この自動typesetは、HTML中のすべての要素に対してただ一度だけ行われるので、content editableでウェブエディタを実装しているケースやリアルタイムプレビューとの相性が非常に悪いのです。つまりcontent editable中のLaTeX記法は置き換えるべきではないのに置き換えてしまうし、プレビューが更新されてもtypesetされないのでこちらはLaTeX記法が処理されません。

そこで細かくMathJaxを制御する必要があるのですが、これがなかなか苦労したので、ハマったことや考えたことをここにメモしておきます。MathJaxは奥が深すぎて同じことをするのに何通りもの方法があり、ここに書いたものが唯一の方法ではありません。実際に実装するときは最新のドキュメントを参照することをおすすめします。

APIリファレンス

リファレンスはこちら。 ぐぐるとよく過去のバージョンのものが出てくることが多いです。何かを探すときは最新版をサイト内検索するのがいいでしょう。

http://docs.mathjax.org/en/latest/api/

SSRかCSRか

MathJax-node があるので、SSR: Server Side Renderingもできるようです。

MathJaxの描画処理はかなり時間がかかり、またpreviewとして仮描画したものを出したりするため、数式の描画を表示するときにかなりガクガクします。

SSRするとこのガクガクがおそらくなくなるので、可能であればSSRしたいところです。しかしそのためには専用のnodejsサーバを立てなければならず、ほかの(たとえばReact SSR)レンダリングサーバに相乗りできるならいいですが、そうではなくMathJaxのためだけに新規にnodejsサーバを立てるのであれば大抵の場合オーバースペックでしょう。

今回はCSR: Client Side Renderingにしました。

MathJaxのロード

MathJaxパッケージはnpmにもありますが、それをwebpackで固めるとextensionのロードが404になるという現象になり、結局解決できませんでした。

なのでcdnjsのようなpublic CDNを使うか、MathJax配布キットのとおりの構造でデプロイするかしないといけないようです。

MathJax 3.0でwebpack対応予定 ということなので、webpackに組み込みたいときは3.0を待つのがいいようです。

自動typesetを無効にする

configに skipStartupTypeset: true を渡します。

MathJax.Hub を設定するためのドキュメントはこちら:

http://docs.mathjax.org/en/latest/options/hub.html

MathJaxの初期化終了コールバック

MathJaxは非同期でコンポーネントをロードするので、 MathJax オブジェクトが存在するからといって使用可能とは限りません。

MathJax.isReady は一つの指標としては使えますが、初期化の終了イベントをフックしたいところです。

初期化シーケンスは Startup Sequence にあり、それぞれのタイイングでコールバックを呼ぶのでイベントリスナを登録すればよさそうです。

このシーケンスの詳細は省きますが、End が初期終了時に呼ばれるイベントです。

MathJax.Hub.Register.StartupHook('End', () => {
  // MathJax APIがすべて利用可能になった!
});

MathJax Typeset API

MathJax.Hub.Typeset(target, callback) を使うと、target elementを指定してtypesetを起動できます。

target は HTML element またはその配列、あるいは null で、 nullの場合は document.body 全体が処理の対象になります。

MathJaxは処理をシーケンシャルにするために MathJax.Callback.Queue というjob queueを持っているので、typesetの処理は実際にはこのようになります *2

MathJax.Callback.Queue(() => {
  MathJax.Hub.Typeset(elements, () => {
    console.log(`typeset is done for ${elements.length} elements.`);
  });
});

エラーハンドリング

プロダクション環境ではエラーがあってもサービス提供者としては何もでないことのほうが多いでしょうが、開発中はエラーの詳細を知りたいというケースでは “Math Processing Error” イベントをフックすることでコンソールにエラーの詳細を出力できます。

MathJax.Hub.Register.MessageHook("Math Processing Error", (message) => {
  console.error(message);
});

MathJax Preview

LaTeXをちゃんとレンダリングするのはかなり時間がかかるようで、デフォルトだと本描画のまえに数回プレビュー描画が入ります。

このpreviewは様々なレイヤでやっているらしく制御の仕方がはっきりしないのですが、 MathJax_Preview CSS classが指定されているので、これに対してスタイルを当てると見た目はそれなりに制御できます。

全体像

というわけで全体像です。実際に運用しているコードそのものではありませんが、だいたいこんな感じです。 requestTypeset() を呼ぶと (1) MathJaxがロードされていなければロードする (2) MathJaxが使えるようになるまで待ってtypesetを起動する (3) requestTypset() は呼ぶたびにtypesetを起動する、ということを行うコードです。

const CONFIG = 'TeX-AMS-MML_HTMLorMML'; // 適当なconfig

const DEV = (process.env.NODE_ENV != 'production');

let isLoading = false;

function loadMathJax(onComplete) {
  const script = document.createElement('script');
  // from https://cdnjs.com/libraries/mathjax
  script.src = `https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js?config=${CONFIG}`;
  script.async = true;
  script.onload = () => {
    const MathJax = window.MathJax;
    console.log(`loaded MathJax/${MathJax.version} (${CONFIG})`);

    MathJax.Hub.Config({
      messageStyle: DEV ? "normal" : "none", // 左下に出るメッセージボックスは開発時のみ有効にする
      skipStartupTypeset: true,
      tex2jax: {
        inlineMath: [
          ['$','$'],
        ],
        displayMath: [
          ['$$', '$$'],
        ],
        processEscapes: true,
      },
    });
    MathJax.Hub.Register.MessageHook("Math Processing Error", (message) => {
      console.error(message);
    });
    MathJax.Hub.Register.StartupHook('End', onComplete);
  };
  document.head.appendChild(script);
}

export function triggerTypeset() {
  const elements = [...document.querySelectorAll('.content')];

  const MathJax = window.MathJax;
  console.assert(MathJax && MathJax.isReady);

  MathJax.Callback.Queue(() => {
    MathJax.Hub.Typeset(elements, () => {
      console.log(`typeset is done for ${elements.length} elements.`);
    });
  });
}

export function requestTypeset() {
  if (!document.querySelector('.content')) {
    return;
  }

  if (window.MathJax && window.MathJax.isReady) {
    triggerTypeset();
  } else {
    if (isLoading) {
      return;
    }
    isLoading = true;

    loadMathJax(() => {
      triggerTypeset();
    });
  }
}

*1:MathJaxの描画処理のことで、原義は「活字を組む」という意味です。MathJaxでこの用語を使うのはTeXに由来すると思われます。

*2:queueを使う必要はもしかしたらないのかもしれませんが、MathJaxは内部で様々なイベントが飛び交う複雑な非同期システムなのでこのようにするのが無難でしょう。

yarnpkgのdependenciesにgithubのリポジトリを直接書こうとしてハマった

追記:(2017/04/14): "foo" : "gfx/foo"$github_user/$repository)というフォーマットでうまくいきました。ε-(´∀`*)ホッ


追記(2017/04/13): 結局解決できてませんでした。つらい。

fooというnpm moduleにパッチを当てた gfx/foo という版をどうしてもいま使いたい、というときに、package.jsonにはこんな感じにURLを直接書けるはずということになっているのですが

{
  "dependencies": {
    "foo": "git+https://github.com/gfx/foo#branch",
  }
}

これがどうもCircleCI上のyarnpkgだと刺さってしまって動きませんでした。yarnpkgのアップグレードなどでも解決せず。結局以下のように .git suffixを付けると解決できました。

{
  "dependencies": {
    "foo": "git+https://github.com/gfx/foo.git#branch",
  }
}