Julia でブラウザに出力する


はじめに

Gadfly.jl のコードを見ていると、ブラウザでの表示が意外と簡単そうだと感じたので、1から実装してみる。

実質的な処理はシンプルで、

  1. 表示したいオブジェクトをもとに、一時ファイル temp.html を作成する
  2. コマンド cmd /c start temp.html を実行する
  3. *.html に関連付けられたブラウザで一時ファイルが開かれる

だけである。

問題となるのは出力先の設定方法で、この記事で詳しく説明していきたい。

[実装したコード]
https://gist.github.com/Lirimy/2962c626941361f8b789fe3f669b5b15

オブジェクトと表現

show の仕事は、対象となるオブジェクトを指定された MIME タイプで io に出力することである。この記事の文脈において、 show は表示に適した形式でオブジェクトを表現する、という解釈が素直だと思う。

struct Hoge end

function Base.show(io::IO, ::MIME"text/html", ::Hoge)
    html = """
    <!DOCTYPE html>
    <html>
        <head>
            <title>Julia Output</title> 
        </head>
        <body>
            Hello, <strong>Julia</strong> !!!
        </body>
    </html>
    """
    write(io, html)
    flush(io)
end

x = Hoge()

Julia / Jupyter での画像表示を実装する
https://qiita.com/Lirimy/items/b5604b41247ba8ca7a00

出力先の管理

Julia での出力先は AbstractDisplay 型のオブジェクトであらわされる。例えば、 REPL の出力部やエディタのプロットペイン、Jupyter などに対応するようなオブジェクトが存在する。

利用できるディスプレイは Base.Multimedia.displays という配列に保持され、表示の際は後ろのほうが優先される。 pushdisplay(d) は、配列の最後尾にディスプレイを追加する。

struct MyDisplay <: AbstractDisplay end

pushdisplay(MyDisplay())

自前のディスプレイを作りたい場合、 display(d, args...) で多重ディスパッチできるような型をつくればよい。実際の処理は display に書いていくことになる。

display メソッドの実装

表示のトリガー

オブジェクト x を表示する際に、 display(x) を出発点として考えよう。おそらく、いろいろな場面で暗黙的に呼ばれていると思われる。

1引数の display(x) は適切なディスプレイ d を選択し、2引数の display(d, x) を呼ぶ。

ここから、ディスプレイに依存した処理に移行する。

MIME の選択

function Base.display(d::MyDisplay, @nospecialize x)
    if showable(MIME("text/html"), x)
        display(d, MIME("text/html"), x)
    else
        throw(MethodError(Base.display, (d, x)))
    end
end

2引数の display(d, x) の役割は、適切な MIME の選択である。ここでは text/html にしか対応していないが、ディスプレイが複数の MIME を表示できるならば、あらかじめ定めた優先度に従って MIME を決定する。

x を表現でき、かつ d に表示できるような MIME が見つからなければエラーを投げるが、その場合は呼び出し元 display(x) が他のディスプレイを試行する。

適切な MIME が選択できれば、3引数の display(d, mime, x) を呼び出す。

出力

3引数の display(d, mime, x) が呼び出された時点で、出力の準備は整っている。

ここでは、 f::IO を一時ファイルとして用意し、 show と接続する。

using DefaultApplication

function Base.display(::MyDisplay, mime::MIME"text/html", @nospecialize x)
    filename = tempname() * ".html"
    open(filename, "w") do f
        show(f, mime, x)
    end
    # run(`cmd /c start $(filename)`) # Windows
    DefaultApplication.open(filename; wait=true)
    nothing
end

# x = Hoge()
display(x)

あとは書き出した一時ファイルを開くだけだが、 OS 依存を避けるために DefaultApplication.jl パッケージを使った。

https://github.com/GiovineItalia/Gadfly.jl/blob/master/src/Gadfly.jl#L1047-L1071
https://github.com/tpapp/DefaultApplication.jl/blob/master/src/DefaultApplication.jl#L12-L24

【補足】 Jupyter での暗黙的な表示

Jupyter では、明示的に display しなくても、セルの戻り値が自動的に表示される。

この記事のコードで挙動を確認すると、自動表示だとディスプレイの優先度が無視されてしまうことがわかった。つまり、display(x) ではなく display(IJulia.InlineDisplay(), x) のように振る舞っている。

IJulia.jl のコードを見たところ、 display を呼んでいるのではなく、もっと低レベルな処理を行っているようで、ここに介入するのは難しそうだと感じた。

display(IJulia.InlineDisplay(), x)
https://github.com/JuliaLang/IJulia.jl/blob/master/src/inline.jl#L93-L99

セル戻り値の暗黙的な表示
https://github.com/JuliaLang/IJulia.jl/blob/4fd955afa1/src/execute_request.jl#L128-L137