📝

Pluto.jlを使うときのTips

2024/12/06に公開

この記事では、(そこそこ普及しているであろう)Pluto.jlノートブックを使うときに役立つTipsを紹介したいと思います。

Pluto.jlとは

まず簡単にPluto.jlの特徴を振り返りたいと思います。

Pluto.jlはJuliaで使える2大ノートブックの一つ(もう一つはJupyter)です。ノートブックとは、コードとその結果を並べて編集や実行を行うことができる環境で、他にはQuartoやMathematicaのノートブックなどがあり科学向けとしてとても使いやすいインターフェースです。

Pluto.jlはJupyterとは違い、Julia言語専用である一方、Juliaに特化した機能があります。
主な特徴としては[1]

  1. セルがリアクティブである(依存するセルが更新されると自動で更新される)
  2. 1.のおかげで、インタラクティブなものが作りやすい(ウェブのUIとしての機能を活用できる)
  3. パッケージ管理が内蔵されていて、かつ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ブロックです。

let_example.jl
x = let x = x, y = z, w
    w = somecalcs(x, y)
    mutation!(w)
    w
end

letは、

  1. 新しいスコープを作る(c.f., begin)
  2. キャプチャできる(letの直後)
  3. 式である

といった特徴があります。
この性質を利用して、letでブロックを作りつつ必要であればletの直後で変数をキャプチャし、ブロックの内部で通常の感覚でmutableを含めたコードを書き、最後に返り値としてその結果を返したものをグローバル変数に束縛するという書き方で、局所的にPlutoの制約を受けないコーディングができます。

letを使った例
letを使った例

プロットの例(Makie)

letブロックが活躍できる例として、MakieをPlutoで使うことを考えてみましょう。

Makie.jlはObservables.jlをベースとしてインタラクティブなUI作れるようになっており、基本的に全てのオブジェクトがmutableです。
また、FigureAxisScatterのように、3種類のオブジェクトを順番に作ることでプロットを作成する体系になっており、特に複雑なプロットを作るときはこれらを直接触ることが多くなります。

これらの特徴はまさしくPlutoと相性が悪いものばかりですが、letを使うことで変に凝らずに素直にMakieを使うことができます。以下にその例を載せています。

Makieをletの中で使う
Makieをletの中で使う

MakieのSpecApi

さて、ここでさらにMakieをPlutoで使う例を掘り下げてみます(飛ばしてもらって構いません)。

letを使う例は確かに普通の感覚でMakieを使う分には良い方法なのですが、Makieの大きな特徴として、(Plots.jlと違って)各オブジェクトはFigureAxisPlotの順に作り、かつそれらは基本的にそれぞれもともとの一つのFigureに属しているというものがあります。つまり複数のScatterを作って、それらを重ねたプロットと並べたプロットを作るということができないのです。

それを解決するのがSpecApiです。

これは去年の11月に追加された機能で、一応実験的な機能ということになっています。

SpecApiでは、通常のMakieの順序とは逆に、Plot(ScatterLines)を作ったあとそれらからAxisなどのBlock(他にLabelLegend)を作り、それらをまとめてGridLayoutで配置してプロットするという用に作成します。

中身は、引数を保持するオブジェクトとして、PlotSpecBlockSpecを作り、最後のplotの段階でそれらを評価してプロットするようになっています。

SpecApiのPlutoにおける使用例
SpecApiのPlutoにおける使用例

このSpecApiの利点としては、

  1. プロットを使い回せる
  2. 複数のセルで分けてプロットを定義できる
  3. レイアウトなどが楽

などがあります。

一方欠点も結構あり、

  1. パフォーマンスが少し良くないらしい
  2. 実装の構造上、補完が全く効かない
  3. 大抵、plot時に評価されるので、エラーメッセージがそこで出てくる
  4. 中身はMakieなのでそれを理解している必要がある
  5. ネストされている分エラーメッセージがわかりにくい
  6. 複雑なものが書けない

と言った感じです。

エディタから編集する

普通のソースコードと比較したときノートブック系が微妙な点として、

  • フロントエンドが普通のテキストエディタではないため、普段と異なる環境になってしまう・カスタマイズ性が乏しい
  • 保存形式が特殊なためフォーマッタなどのツールがそのままでは使えない・gitとの相性が悪い

と言ったものがあります。

しかし、Plutoは保存形態が普通のJuliaのソースコードとして評価できるテキストファイルであるため、これらの欠点はありません。
ただし、Plutoはデフォルトではファイルの更新をノートブックに反映させないため、以下の関数で開始するようにします。

Pluto.run(auto_reload_from_file = true)

また、注意点として、Pluto側で実行が終わったときにファイルの保存も行われるので、エディタ側はそれを読み込むようにしないと[2]編集の競合が起きます。

テキストエディタで編集するときの利点としては、以下のようなものがあります。

  • 言語サーバーの恩恵が受けられる
  • より高度なシンタックスハイライト
    • 例えば、tree-sitterのinjectionの機能を使えば、@r_strに対して、正規表現のシンタックスハイライトが適用されます
  • さらに、エディタ用の汎用のプラグインが使える
    • Unicodeシンボルの入力に別のものが追加できる
なぜPlutoはプレーンテキストで保存されるのか・なぜ他のノートブックはプレーンテキストではないのか

Plutoは実行結果を保存しないからです。
セルの実行順は先述の通り変数などの依存関係を元に決定され、かつ全てのセルは編集と同時に実行されるようになっており、かつパッケージマネージャーが内蔵されていて常に再現性が保証された状態なので、結果を保存しなくても共有できるという思想だと思っています。

ただ、それでも結果が見れないのは不便なので、HTMLやPDFでエクスポートする機能があります。

通常のEnvironmentの管理と統合する

PlutoはデフォルトでノートブックごとにJuliaの環境を作り、usingimportをすると自動でaddすることで必要なパッケージを自動で管理しています。
通常の環境におけるProject.tomlEnvironment.tomlはファイルの下の方に文字列として埋め込まれています。

これは再現性を担保する大事な機能なのですが、独自のパッケージやモジュールを使いたいときはどうすればよいのでしょうか。

そのときは普通にusing Pkgをして、普通のJuliaの環境を管理するときと同じようにすれば良いです。
Pkg.activatePkg.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

とすれば元のプロジェクトと同じ環境が使えます。

https://plutojl.org/en/docs/packages-advanced/

Latexifyの活用(あるいは一般にMultimedia I/O用のshowメソッド活用)

PlutoはJulia組み込みのshowを活用することで、ノートブック用のリッチな表示を行っています。
基本的にセルの返り値を表示しているだけです。

https://docs.julialang.org/en/v1/base/io-network/#Multimedia-I/O

https://discourse.julialang.org/t/how-does-pluto-decide-what-to-show-graphically/58033/5?u=qwjyh

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です。

のPlutoにおける使用例
@latexdefineのPlutoにおける使用例

多少不格好ですが、教科書などに載ってる式に近い形で表示されるので、視認性が大変良いです。

以下、いろんな型のものに対する表示の例です。

PeriodicTables.jlの
PeriodicTables.jlのElement
Colors.jlのと
Colors.jlのColorNamedTuple
(たくさんの行や列も表示できる)
DataFrame(たくさんの行や列も表示できる)

ロガーの活用

Plutoにはとても便利なロガーが内蔵されているので、デバッグではprintln@showよりも@infoなどが使いやすいです。

ログはstdoutと同様にセルの下側に表示されます。
ログに与えた値は普通にセルが返した値と同様にインタラクティブに表示されます。
また、ログが出た行もハイライトしてくれるようになっています。

Plutoにおけるロガーの使用例。ホバーすると対応する行がハイライトされる。
Plutoにおけるロガーの使用例。ホバーすると対応する行がハイライトされる。

おわりに

Plutoを使う上で役に立つかもしれないTipsをまとめてみました。

例に使ったノートブックはここ(HTML出力後)ここ(git repo)で見れます。


このブランチでNeovimとtree-sitterを使って、Plutoのノートブックを編集する簡易的なものを書いていたんですけど、バグが多い状態で放置してしまってます。
いつか完成させたい(かも)。

脚注
  1. こっちにも書いてます。 ↩︎

  2. たとえばNeovimでは、エディタにフォーカスし直すと読み込んでくれます。 ↩︎

GitHubで編集を提案

Discussion