はじめに
Gadfly.jl のコードを見ていると、ブラウザでの表示が意外と簡単そうだと感じたので、1から実装してみる。
実質的な処理はシンプルで、
- 表示したいオブジェクトをもとに、一時ファイル
temp.html
を作成する - コマンド
cmd /c start temp.html
を実行する - *.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