📒

Livebook 他のノートのモジュール定義を使うための方法案

に公開

Livebookで作業していると「他のノートで定義した関数を使いたい」「共通モジュールを定義して使いまわしたい」ことがあります。

(LivebookはElixirで駆動するJupyterNotebookのようなWebアプリケーションです。最近では、この説明で良いのかわからないくらい充実してきました)

Livebookのruntime設定がプロジェクトな環境(attached)であればそっちに用意すれば済みますが、Dockerなどで単体動作させているケース(standalone)では簡単ではありません。そして、手元でさくっと動かすときは単体動作が多いです。

ということで、他のノートのモジュール定義を頑張って使おうという記事です(インターネットのどこかに情報がありそうですが、、、)
=> 方法6が私の結論になります。

📝 各ノートの実行がstandaloneなとき、それぞれのruntimeは独立して動作する(単に異なるノードとして動作する)。

方法1: ノートのruntimeをつなぐ

どこかでみた記憶があります、二(N)番煎じです。今でも可能です。

  1. ライブラリ用ノートを書いて実行し、Node.self()でノード名をみる。

  1. 呼び出し側は、上記で得たノード名を指定して関数を実行する。

:rpc.call(:"livebook_67nmvpoe--nwrw4wux@127.0.0.1", MyLib, :myname, [])

方法比較のための準備として、特徴をかくと

  • ライブラリとして記述したノートを起動時に毎回開いて実行する(runtimeを起動する)必要がある
  • ノード名が毎回変わる(自分で指定して実行することも確かできます)
  • ノード名の指定に手間がかかる
  • とはいえ考え方が王道

方法2: アプリとしてLivebook起動時に動かして、そのノードにつなぐ

方法2は、方法1のノートをアプリとして自動起動するようにしておく、というものです。

少し前からLivebookでアプリ化したノートを動かせるようになりました。そして、アプリはLivebook起動時に自動で実行できます。

例えば、方法1で作成したライブラリノートをアプリとしてslug("my-lib")を設定し、books/apps/以下に保存します。そしてLIVEBOOK_APPS_PATH={pwd}/books/apps/などと指定してLivebookを再起動すると、ライブラリノートがアプリとして自動実行されます。

副作用として、アプリとしてslugに順守したURLも発行されます。ですが、まぁここでは無視します。アプリを支えるノードが自動で作られていることが大事です。

あとは使いたいノート側で探して繋ぐだけです。ただし、シンプルとはいえません。

# ここではアプリのslug名から取り出す。他に例えばノートタイトルから`list_sessions`を使って取れる
my_lib_app = "my-lib"

# runtimeがデフォルトで繋がっているのは親=Livebookのみなので利用している。その他`Node.self()`からも探せそう
[livebook] = Node.list(:connected)

# 親のLivebookがもつアプリ情報から、ライブラリアプリのnode情報を取り出し
app = livebook |> :rpc.call(Livebook.Apps, :list_apps, []) |> Enum.find(& &1.slug == my_lib_app)
[app_session] = app.sessions
runtime_node = :sys.get_state(app_session.pid).data.runtime.node

# 実行
:rpc.call(runtime_node, MyLib, :myname, [])
  • 自動で毎回ライブラリノートを起動する必要がない
  • slugでつけた名前で接続できるので簡単、ただし、コードが長い

方法3: Mix.installでパッケージとして導入する案

方法2の各処理を関数化してパッケージに入れ込む案です。setupセルでMix.installして用意した関数をサクッと使う想定です。方法5を思いついたので実現していません、アイディアとして。

方法4: Mix.installでパッケージとしてライブラリ化する案

方法1,2,3とは少し方針を変更し、特定フォルダの.livemdをライブラリ化するパッケージを導入するというものになります。

下記パッケージを作る。
- パッケージ側で独自コンパイラを定義する
- Mix.installされるときに特定フォルダ(`pwd/books/lib`)のlivemdを全て読んで、`defmodule`な内容のみをコンパイルする
- 再コンパイルする関数も提供する

などと構想しました。が、途中で方法5を思いついたので実現していません、アイディアとして。

方法5: アプリとしてLivebook起動時に、Livebook側にコンパイルさせる

方法2の欠点は「都度アプリを動かしているノードを特定する必要がある」ですが、そもそもアプリとして起動時にLivebookノードにモジュールごとコンパイルさせてしまおう、という発想になります。Livebookノードまでなら特定が簡単です。

よって、アプリとして動かすライブラリ側のノートの方に一工夫いれます。

[livebook] = Node.list(:connected)

defmodule MyLib2 do
  def myname do
    "my_lib2"
  end
end
|> then(fn {_, module, bytecode, _functions} ->
  :rpc.call(livebook, :code, :load_binary, [module, nil, bytecode])
end)

やっていることは定義したあとのbytecodeをもらって、Livebookノードでコンパイルです。

これで実際に使うノート側は単にLivebookノードでcallするだけになります。

[livebook] = Node.list(:connected)
:rpc.call(livebook, MyLib2, :myname, [])

シンプルなモジュール(つまりElixir標準機能のみを使うモジュール)では、いい感じに使えそうです。

方法6: アプリとしてLivebook起動時に、Livebook側に呼び出し用関数をコンパイルさせる

方法5で終わったつもりでしたが、パッと閃いたものには欠点があるある。

Livebook側にモジュールごと注入するということは、当然そのライブラリブックでMix.installしたパッケージが使えないことになります。Livebook側ではインストールされていないためです。

例えばStatisticsを使う関数を共通利用したいとして、

Mix.install([
  { :statistics, "~> 0.6"}
])
[livebook] = Node.list(:connected)

defmodule MyLib3 do
  def myname do
    "my_lib3"
  end
  
  def mean(list) do
    Statistics.mean(list)
  end
end
|> then(fn {_, module, bytecode, _functions} ->
  :rpc.call(livebook, :code, :load_binary, [module, nil, bytecode])
end)

のように方法5をしても、StatisticsはLivebookなノードではインストールされていないので、呼ぶと、

{:badrpc, {:EXIT, {:undef, [{Statistics, :mean, [[1, 2, 3]], []}, {MyLib3, :mean, 1, []}]}}}

が返ることになります。よって、しぶしぶ方法4構想復活、ではなく、Livebookノードには呼出用関数だけを注入して、実際にはアプリのノードで実行されるようにして、対応します。

my_node = Node.self()
[livebook] = Node.list(:connected)

call_source =
"""
defmodule MyLib3 do
  def call(function, args) do
    :rpc.call(:"#{my_node}", MyLib3, function, args)
  end
end
"""

:rpc.call(livebook, Code, :compile_string, [call_source])

こうすることで使用側はcallを介して呼び出します。

[livebook] = Node.list(:connected)
:rpc.call(livebook, MyLib3, :call, [:myname, []])

values = [1,2,3]
:rpc.call(livebook, MyLib3, :call, [:mean, [values]])

一段ややこしくなるのと、各ライブラリファイルに記述が増えますが、まぁ許容範囲かなと思います。


以上です。これで独立したLivebookでもモジュールをうまく共通利用できるようになったはずです。使ってみて不備あればまた更新します。

Discussion