markdownの拡張記法の現状

Kibelaは次のようにいくつかmarkdownを拡張しています。

そして、今後もそういう拡張は増えていくと思われます。

PlantUML

KibelaのPlantUML記法はこういうやつです。

    ```plantuml
    Bob -> Alice : hello
    Alice -> Bob : Go Away
    ```

GitLabも同じ構文でPlantUMLをサポートしていますね。

PlantUML & GitLab - GitLab Documentation

crowiも最近PlantUML記法をサポートしはじめましたね。構文はKibelaとおなじです。

Support PlantUML by sotarok · Pull Request #231 · crowi/crowi · GitHub

esa.io もUML記法という名前でPlantUMLをサポートしていて、こちらは plantuml ではなく uml というキーワードです。

ReleaseNotes/2016/10/22/UML記法に対応しました - docs.esa.io

問題としては、fenced code blocksでいいの?といいうところです。「markdownとして解釈されずにコードを表示する」というfenced code blocksのセマンティクスを完全に無視していますからね。

具体的な弊害としても、PlantUMLそのもののソースコードをsyntax highlightを効かせて埋め込むことができないというものがあります。

MathJax / TeX

こちらは様々なmarkdown処理系が思いおもいに拡張していて、 だいたい Math in MarkDown · cben/mathdown Wiki · GitHub にまとまっています。

CommonMarkでも2014年くらいから議論されていますが進捗は芳しくなさそうです。

MathJax extension for LaTeX equations - Extensions - CommonMark Discussion

ざっくりまとめると、fenced code blocksを使うスタイルと $$ ... $$\( ... \) などの独自記法を定義するスタイルがあります。

fenced code blocksの問題はPlantUMLとおなじです。つまり、セマンティクスを無視していいのかという一貫性の問題と、syntax highlightとの衝突の問題です。 もっとも、TeX記法に関していうと math キーワードを使う事が多いようなので、TeX (LaTeX) とのsyntax highlightの衝突はそれほど問題ではありません。

独自記法は大きな問題です。 $$ はshellやPerlのpidを示す特殊変数なので、普通に文中に出現する可能性があります。また、markdownは \ をメタ文字として扱っているので、 \(\[ というトークンはmarkdown parserレベルで ([ にしてしまいます。つまり、真面目にやるならmarkdown parser自体を拡張するか、markdown parserに与える前に処理をする必要があります。これらはいずれにしても実装コストが高いので避けたいところです。github/cmarkのtable拡張 をみても、パーサの拡張はわりとしんどい感じがします。

R Markdown

R Markdown というものがあって、ツールとしてはJupiter Notebookみたいなものだと認識していますが、markdown中にRを書けて実行もできるというものです。

構文がちょっとおもしろくて、fenced code blocksなんですが明らかに拡張だなとわかります。

    ```{r}
     1 + 1
    ```

RだけじゃなくPythonやbashなどもいけるようですね。

{r INCLUDE = FALSE} のように、code blockに対して引数を与えることもできるようです。

個人的には、この構文はわりと使いやすいようにみえます。つまり

  • fenced code blocksの拡張だがそれほどセマンティクスを侵害していないようにみえる(主観)
  • foo に対する {foo} で「実行」を示すので、syntax highlightと衝突しない
  • fenced code blocksの中はmarkdownの干渉をうけないのでmarkdown parserの拡張が不要

もちろんデメリットもあって

  • インライン記法がないことで、文中にLaTeXで数式を書きたいというニーズにこたえられない
  • Kibelaに限って考えるとしても、これまでやってきた拡張と互換性がない
  • CommonMarkとしてのコンセンサスはない

という感じです。CommonMark的にどうかというのは気持ちの問題なので無視してもいいですが、他のデメリットもそこそこ大きいんですよね。

Generic Directives /Plugins in CommonMark

汎用的な拡張記法の提案で、CommonMarkで議論中のものです。

Generic directives/plugins syntax - Extensions - CommonMark Discussion

ざっとしか眺めてませんが、あまり芳しくなさそうです。スレ主の提案はこんな感じです。

::: name [inline-content] {key=val}
contents, which are sometimes further block elements
:::

悪くなさそうにみえますが、2014年の議論でいままとまっていないということは…。

あまりまとまりはないですが、とりあえず現状のメモでした。

See Also

gfx.hatenablog.com

TypeScriptのDefinitelyTypedは「ダメでもともと、うまく使えればラッキー」くらいの距離感がよい

自前でTypeScript型定義ファイル(dts)を用意していないJSライブラリのための型定義ファイル集があります。

https://github.com/DefinitelyTyped/DefinitelyTyped

npmで @types/react みたいなのがそうです。

これは便利なものですが、ライブラリ作者ではない第三者が作っていることがほとんど(作者にやる気があればライブラリ本体で管理するほうが圧倒的によいので!)なので、型定義が間違ってることが普通にあります。

直近だと、 @types/react-intl の型定義が間違っているので、これを導入するとコンパイルが通らなくなるみたいな事情がありました。

一応PRは送りましたが、これがマージされても別に使う必要ないんじゃないかと思ってます。

というのも「第三者が型定義ファイルを作り気合で追従していく」という仕組み上、どうやってもTypeScriptで書いたライブラリと比較して型情報の精度は落ちるからです。DefinitelyTypedはあまり信頼できないものと割り切るしかありません。

そして、現在は TypeScript compilerの --allowJs フラグ(あるいは tsconfig.jsonallowJs option)によって *.js をimportできるようになっています。

おそらくもう少し時間がたつと の --allowJs が賢くなり、素のJSでもある程度型を推論してよしなにしてくれるようになると思われます。

つまり、

  • DefinitelyTypedは過渡期の技術であり、平均的な品質は低く、将来的には廃れると思われる
  • @types/* にこだわる必要はなく、便利そうなら使ってもいいが型定義なしでもまったく問題ない

くらいの気持ちで付き合うと気が楽でいいかなと思います。

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