Livebook 他のノートのモジュール定義を使うための方法案
Livebookで作業していると「他のノートで定義した関数を使いたい」「共通モジュールを定義して使いまわしたい」ことがあります。
(LivebookはElixirで駆動するJupyterNotebookのようなWebアプリケーションです。最近では、この説明で良いのかわからないくらい充実してきました)
Livebookのruntime設定がプロジェクトな環境(attached)であればそっちに用意すれば済みますが、Dockerなどで単体動作させているケース(standalone)では簡単ではありません。そして、手元でさくっと動かすときは単体動作が多いです。
ということで、他のノートのモジュール定義を頑張って使おうという記事です(インターネットのどこかに情報がありそうですが、、、)
=> 方法6が私の結論になります。
📝 各ノートの実行がstandaloneなとき、それぞれのruntimeは独立して動作する(単に異なるノードとして動作する)。
方法1: ノートのruntimeをつなぐ
どこかでみた記憶があります、二(N)番煎じです。今でも可能です。
- ライブラリ用ノートを書いて実行し、
Node.self()
でノード名をみる。
- 呼び出し側は、上記で得たノード名を指定して関数を実行する。
: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でつけた名前で接続できるので簡単、ただし、コードが長い
Mix.install
でパッケージとして導入する案
方法3: 方法2の各処理を関数化してパッケージに入れ込む案です。setupセルでMix.install
して用意した関数をサクッと使う想定です。方法5を思いついたので実現していません、アイディアとして。
Mix.install
でパッケージとしてライブラリ化する案
方法4: 方法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