Pluto.jlを使うときのTips
この記事では、(そこそこ普及しているであろう)Pluto.jlノートブックを使うときに役立つTipsを紹介したいと思います。
Pluto.jlとは
まず簡単にPluto.jlの特徴を振り返りたいと思います。
Pluto.jlはJuliaで使える2大ノートブックの一つ(もう一つはJupyter)です。ノートブックとは、コードとその結果を並べて編集や実行を行うことができる環境で、他にはQuartoやMathematicaのノートブックなどがあり科学向けとしてとても使いやすいインターフェースです。
Pluto.jlはJupyterとは違い、Julia言語専用である一方、Juliaに特化した機能があります。
主な特徴としては[1]、
- セルがリアクティブである(依存するセルが更新されると自動で更新される)
- 1.のおかげで、インタラクティブなものが作りやすい(ウェブのUIとしての機能を活用できる)
- パッケージ管理が内蔵されていて、かつ1.のおかげで、再現性に優れている
と言ったものが挙げられます。
一方で、この特徴を実現するためにJupyterやノートブック以外の環境にはない制約や機能があるため、それらとは違った心持ちで使うと良いこともあります。
この記事では個人的にPlutoを使う中で見つけた、それらPlutoを使う上で役に立つ情報を紹介したいと思います。
グローバル変数は、immutableにする
Pluto.jlではリアクティブさを実現するために、各セルの間の変数の依存関係を解析しています(右下のStatusで"Evaluating cells"の中に"resolve_topology"があります)。Pluto.jlでは各セルの依存関係・実行順序は変数の依存関係から決められており、そのおかげで各セルを実行順序と関係なく並べ替えることができるようになっています。
一方、この依存関係の解析に使う変数がmutableなものになっていると、解析がうまくいかず、mutable変数に対する更新に対して他のセルが自動実行されない状態になってしまうことがあります(リアクティブさが失われます)。
また、同じ名前の変数を複数回定義することもできなくなっています。
リアクティブさが失われている例
これらの特徴を踏まえると、Juliaの変数は基本的にmutableですが、特にグローバルな(複数のセルで使われる)変数は、(ノートブックを1回実行する過程で)immutableとして扱うことが推奨されます。
ノートブックを使うセッションとしては、const
をつけても更新ができるので、全部にconstをつけるのも良いかもしれません。
let
ブロックを使う
この推奨事項のもとでは
- 全部の変数に一意の名前をつける必要がある
-
Array
とかの処理がとても大変になる(forループは基本的に使えない)
というかなり厳しい制約のもとでコードを書くことになってるように思えるかもしれません。
この制約を部分的に緩和するのに使えるのが、let
ブロックです。
x = let x = x, y = z, w
w = somecalcs(x, y)
mutation!(w)
w
end
let
は、
- 新しいスコープを作る(c.f.,
begin
) - キャプチャできる(
let
の直後) - 式である
といった特徴があります。
この性質を利用して、let
でブロックを作りつつ必要であればlet
の直後で変数をキャプチャし、ブロックの内部で通常の感覚でmutableを含めたコードを書き、最後に返り値としてその結果を返したものをグローバル変数に束縛するという書き方で、局所的にPlutoの制約を受けないコーディングができます。
letを使った例
プロットの例(Makie)
let
ブロックが活躍できる例として、Makie
をPlutoで使うことを考えてみましょう。
Makie.jlはObservables.jlをベースとしてインタラクティブなUI作れるようになっており、基本的に全てのオブジェクトがmutableです。
また、Figure
→Axis
→Scatter
のように、3種類のオブジェクトを順番に作ることでプロットを作成する体系になっており、特に複雑なプロットを作るときはこれらを直接触ることが多くなります。
これらの特徴はまさしくPlutoと相性が悪いものばかりですが、let
を使うことで変に凝らずに素直にMakieを使うことができます。以下にその例を載せています。
Makieをletの中で使う
MakieのSpecApi
さて、ここでさらにMakieをPlutoで使う例を掘り下げてみます(飛ばしてもらって構いません)。
let
を使う例は確かに普通の感覚でMakieを使う分には良い方法なのですが、Makieの大きな特徴として、(Plots.jlと違って)各オブジェクトはFigure
→Axis
→Plot
の順に作り、かつそれらは基本的にそれぞれもともとの一つのFigure
に属しているというものがあります。つまり複数のScatter
を作って、それらを重ねたプロットと並べたプロットを作るということができないのです。
それを解決するのがSpecApi
です。
これは去年の11月に追加された機能で、一応実験的な機能ということになっています。
SpecApiでは、通常のMakieの順序とは逆に、Plot
(Scatter
やLines
)を作ったあとそれらからAxis
などのBlock
(他にLabel
やLegend
)を作り、それらをまとめてGridLayout
で配置してプロットするという用に作成します。
中身は、引数を保持するオブジェクトとして、PlotSpec
やBlockSpec
を作り、最後のplot
の段階でそれらを評価してプロットするようになっています。
SpecApiのPlutoにおける使用例
このSpecApiの利点としては、
- プロットを使い回せる
- 複数のセルで分けてプロットを定義できる
- レイアウトなどが楽
などがあります。
一方欠点も結構あり、
- パフォーマンスが少し良くないらしい
- 実装の構造上、補完が全く効かない
- 大抵、
plot
時に評価されるので、エラーメッセージがそこで出てくる - 中身はMakieなのでそれを理解している必要がある
- ネストされている分エラーメッセージがわかりにくい
- 複雑なものが書けない
と言った感じです。
エディタから編集する
普通のソースコードと比較したときノートブック系が微妙な点として、
- フロントエンドが普通のテキストエディタではないため、普段と異なる環境になってしまう・カスタマイズ性が乏しい
- 保存形式が特殊なためフォーマッタなどのツールがそのままでは使えない・gitとの相性が悪い
と言ったものがあります。
しかし、Plutoは保存形態が普通のJuliaのソースコードとして評価できるテキストファイルであるため、これらの欠点はありません。
ただし、Plutoはデフォルトではファイルの更新をノートブックに反映させないため、以下の関数で開始するようにします。
Pluto.run(auto_reload_from_file = true)
また、注意点として、Pluto側で実行が終わったときにファイルの保存も行われるので、エディタ側はそれを読み込むようにしないと[2]編集の競合が起きます。
テキストエディタで編集するときの利点としては、以下のようなものがあります。
- 言語サーバーの恩恵が受けられる
- 定義ジャンプ
- シンボルの一括リネーム(特に、Plutoでは同じ名前の変数を複数定義できないので、重要です)
- その変数を参照してるものをリスト
- フォーマット
- PlutoでTabを使うとTabで保存されてしまいます
- などなど
- より高度なシンタックスハイライト
- 例えば、tree-sitterのinjectionの機能を使えば、
@r_str
に対して、正規表現のシンタックスハイライトが適用されます
- 例えば、tree-sitterのinjectionの機能を使えば、
- さらに、エディタ用の汎用のプラグインが使える
- Unicodeシンボルの入力に別のものが追加できる
なぜPlutoはプレーンテキストで保存されるのか・なぜ他のノートブックはプレーンテキストではないのか
Plutoは実行結果を保存しないからです。
セルの実行順は先述の通り変数などの依存関係を元に決定され、かつ全てのセルは編集と同時に実行されるようになっており、かつパッケージマネージャーが内蔵されていて常に再現性が保証された状態なので、結果を保存しなくても共有できるという思想だと思っています。
ただ、それでも結果が見れないのは不便なので、HTMLやPDFでエクスポートする機能があります。
通常のEnvironmentの管理と統合する
PlutoはデフォルトでノートブックごとにJuliaの環境を作り、using
やimport
をすると自動でadd
することで必要なパッケージを自動で管理しています。
通常の環境におけるProject.toml
やEnvironment.toml
はファイルの下の方に文字列として埋め込まれています。
これは再現性を担保する大事な機能なのですが、独自のパッケージやモジュールを使いたいときはどうすればよいのでしょうか。
そのときは普通にusing Pkg
をして、普通のJuliaの環境を管理するときと同じようにすれば良いです。
Pkg.activate
やPkg.add
をすると、Plutoのパッケージ管理機能はオフになります。
例えば独自のパッケージを実装しているときは、その中にノートブックを置いてその環境を参照するようにします。
以下のような構成だと、
tree -L 2
.
├── CHANGELOG.md
├── Manifest.toml
├── Project.toml
├── README.md
├── docs
│ ├── Manifest.toml
│ ├── Project.toml
│ ├── build
│ ├── make.jl
│ └── src
├── examples
│ ├── ...
│ └── notebook.jl
├── src
│ ├── MyPackage.jl
│ └── ...
└── test
└── runtests.jl
begin
using Pkg
Pkg.activate("..")
end
とすれば元のプロジェクトと同じ環境が使えます。
Latexifyの活用(あるいは一般にMultimedia I/O用のshowメソッド活用)
PlutoはJulia組み込みのshow
を活用することで、ノートブック用のリッチな表示を行っています。
基本的にセルの返り値を表示しているだけです。
JupyterではMarkdownセルと分かれているところもJulia標準ライブラリのDocs
にある@md_str
と、それが返すMarkdown
を表示することで実現しており、とてもシンプルです。
# 中身はただのJuliaコード
md"""
# Markdownのセル
- tree-sitter injectionを使っていれば、ここにMarkdownのシンタックスハイライトが適用される!
- Zennで使われてるシンタックスハイライトは複数行の文字列リテラルに対応してなさそう
- tree-sitterのinjectionで壊れていた部分が治ったみたいです: https://github.com/nvim-treesitter/nvim-treesitter/pull/7390
"""
数あるshowメソッドの中でも特に強力だと思うのが、Latexifyの活用です。
科学で出てくる式を数値計算するときに、コードへの翻訳でミスをするというのはよくあることだと思います。
JuliaではUnicode文字を積極的に使えるので、かなり良くはなりますが、それでも関数呼び出しをたくさんネストしたり項がたくさんあったりすると、どうしてもエラーが起きてしまいます。
その時に便利なのが@latexdefine
や@latexrun
です。
@latexdefine
のPlutoにおける使用例
多少不格好ですが、教科書などに載ってる式に近い形で表示されるので、視認性が大変良いです。
以下、いろんな型のものに対する表示の例です。
PeriodicTables.jlのElement
Colors.jlのColor
とNamedTuple
DataFrame
(たくさんの行や列も表示できる)
ロガーの活用
Plutoにはとても便利なロガーが内蔵されているので、デバッグではprintln
や@show
よりも@info
などが使いやすいです。
ログはstdout
と同様にセルの下側に表示されます。
ログに与えた値は普通にセルが返した値と同様にインタラクティブに表示されます。
また、ログが出た行もハイライトしてくれるようになっています。
Plutoにおけるロガーの使用例。ホバーすると対応する行がハイライトされる。
おわりに
Plutoを使う上で役に立つかもしれないTipsをまとめてみました。
例に使ったノートブックはここ(HTML出力後)やここ(git repo)で見れます。
このブランチでNeovimとtree-sitterを使って、Plutoのノートブックを編集する簡易的なものを書いていたんですけど、バグが多い状態で放置してしまってます。
いつか完成させたい(かも)。
Discussion