Julia でブラウザに出力する

# はじめに

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

![ブラウザ出力](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202534/5bffe9a2-1069-8e42-2c3d-4aaafc3ca268.png)

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

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

だけである。

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

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

# オブジェクトと表現

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

```julia
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)` は、配列の最後尾にディスプレイを追加する。

```julia
struct MyDisplay <: AbstractDisplay end

pushdisplay(MyDisplay())
```

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

https://github.com/GiovineItalia/Gadfly.jl/blob/master/src/Gadfly.jl#L998

# display メソッドの実装

## 表示のトリガー

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

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

https://github.com/JuliaLang/julia/blob/master/base/multimedia.jl#L324-L336

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

## MIME の選択

```julia
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
```

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

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

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

https://github.com/GiovineItalia/Gadfly.jl/blob/master/src/Gadfly.jl#L1007-L1025

## 出力

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

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

```julia
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