エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

ElixirのログをOTP標準装備のハンドラでファイルに出力する覚え書き

数年前に、Elixir の Logger モジュールのカスタムバックエンドを書く、という記事を書きました。

blog.emattsan.org

ごく最近になって。 Elixir Forum で Logger の設定の記事を読み、OTP が標準で用意しているハンドラを使えば、バックエンドを書かなくてもログをファイルに出力できると知りました。 調べてみると、ハンドラは OTP 21.0 で追加された模様。

奇しくも OTP 21.0 のリリース日は、先の記事の公開日と同じ 2018/06/19 だったことに何かの縁を感じたり感じなかったり。

いまだ全貌を掴みきれていないのですが、ログをファイルに出力する最低限の設定を確認したので、忘れないうちに記録しておきます。

道具立て

logger_std_h モジュール

ハンドラには Erlang のドキュメントの次のページで紹介されている logger_std_h モジュールを利用します。

www.erlang.org

設定項目がいくつかあるのですが、ここでは出力先のファイル名を指定する file という項目のみ利用します。

ハンドラを追加する方法

Erlanglogger モジュールの add_handlers/1 関数を利用してハンドラを追加登録します。

www.erlang.org

ドキュメントに "Reads the application configuration parameter logger and calls add_handlers/1 with its contents." とあるように、設定は config ファイルに記述することになります。

設定の記述法

設定はどのように記述するのか。 logger:add_handlers/1 関数をどこで呼び出すのか。 これらは、Elixir の Logger モジュールのドキュメントに記述があります。

hexdocs.pm

Erlang/OTP handlers must be listed under your own application:

config :my_app, :logger, [
  {:handler, :name_of_the_handler, ACustomHandler, configuration = %{}}
]

And then, explicitly attached in your Application.start/2 callback:

:logger.add_handlers(:my_app)

見ての通り、ドキュメントの記述は Application モジュールを利用することを前提としています。 logger:add_handlers/1 関数を適当なタイミングで呼び出せれば Application モジュールを使わなくてもよいようですが、ここではドキュメントにならって Application ありで書いてゆきます。

実際にログをファイルに出力してみる

プロジェクトを用意する

Application モジュールを利用する雛形を生成するため、--sup オプション付きで mix new コマンドを実行します。

$ mix new my_app --sup
$ cd my_app

config/config.exs ファイルを書く

logger_std_h モジュールを利用する設定を config/config.exs に記述します。

コード中 config: %{...} で記述しているオプションは、Erlang のドキュメントにあったオプションの内容です。 Erlang のオプションであるため、ファイル名は Elixir の文字列(バイナリ)ではなく、Erlang の文字列(文字リスト)で指定する必要があります。

import Config

config :my_app, :logger, [
  {
    :handler,
    :my_log,       # ハンドラを識別するための ID
    :logger_std_h, # 利用するハンドラ
    %{
      config: %{
        # 注意! ファイル名は、String (binary) でなく charlist で指定する
        file: 'log/my_log.log'
      }
    }
  }
]

lib/my_app/application.ex にハンドラを追加するコードを書く

logger:add_handlers/1 関数を呼び出すコードを lib/my_app/application.ex に追加します。

defmodule MyApp.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    :logger.add_handlers(:my_app) # この行を追加

    children = [
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

ここまで記述できたら IEx を起動します。

$ iex -S mix

ハンドラが追加されていることを確認する

登録されているハンドラは logger:get_handler_ids/0 関数で確認することができます。

iex> :logger.get_handler_ids()
[:my_log, Logger]

config/config.exs に記述したハンドラの ID が表示されると思います。 そして、見ての通り、Elixir 標準の Logger も登録されていることがわかります。

この方法は、起動済みのハンドラを変更することはせず、あくまで追加で登録するためのもののようです。

ログを出力してみる

準備ができたのでログを出力して確認してみます。

iex> require Logger                       
Logger
iex> Logger.info("Hello")                 

19:28:42.693 [info] Hello
:ok

標準のハンドラも有効なので、コンソールにログが出力されました。

ファイルを確認してみます。

iex> File.read!("log/my_log.log") |> IO.puts()
2023-01-30T19:28:37.413517+09:00 info: Application: my_app. Started at: nonode@nohost.
2023-01-30T19:28:42.693326+09:00 info: Hello

:ok

ファイルには、Logger.info/1 で出力する前にアプリケーションが起動した時のログも記録されていました。

ディスク用のハンドラもある

ここでは logger_std_h モジュールを使いましたが、それとは別にディスク出力向けのハンドラ logger_disk_log_h も用意されています。

www.erlang.org

ドキュメントを見てみると、ログローテーションをはじめディスクに出力することを意識したとおぼしきオプションが並んでいます。 こちらのハンドラも折を見て試してみようと思います。

いつか読むはずっと読まない:Science と Fiction のはざま

しまった。 まだ「三体」読んでない。

LiveViewでTaskの結果はhandle_infoで受ければよいという話

LiveViewでTaskの結果はhandle_infoで受ければよいという話

ElixirWeekly の何号だったか失念してしまったのですが。

LiveView のプロセスで、非同期処理を Task.async/1 で実行したならば、Task.async/1 が終了時に送信するメッセージを受け取ればよい、というお話。

プロセスのふるまいを学んだときに、どのようなメッセージが送られるか問いことを覚えたはずなのに、文脈が変わっただけでこうも気付けなくなるものかと思い知った次第。

おさらい Task.async/1

Task.async/1 を実行すると、Task 構造体が返ります。

構造体の中のキー :ref にタスクを識別するリファレンスが格納されています。

iex(1)> Task.async(fn -> :timer.sleep(100); 123 end)
%Task{
  owner: #PID<0.109.0>,
  pid: #PID<0.111.0>,
  ref: #Reference<0.1023354925.2953576457.207757>
}

タスクが終了すると、リファレンスとタスクの最後の値のペアがメッセージとして送られてきます。

最後にタスクのプロセスが終了したことの通知 :DOWN が送られます。

iex(2)> flush
{#Reference<0.1023354925.2953576457.207757>, 123}
{:DOWN, #Reference<0.1023354925.2953576457.207757>, :process, #PID<0.111.0>,
 :normal}
:ok

これをふまえて

初期状態 stopping のときに START ボタンを押すと、starting と表示されのち1秒後に running の表示に換わるサンプルです。

Task.async/2 の戻り値に含まれるリファレンスを保存しておきます。 受信したメッセージにタスクのリファンレンスが含まれていれば、先に起動したタスクの結果のメッセージなので、処理をします。

プロセス終了時の :DOWN を含むメッセージも送られてくるので、そのメッセージにマッチする handle_info/2 を用意しておかなければなりません。 ここでは他のすべてのメッセージは無視するようにしています。

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, status: "stopped", ref: nil)}
  end

  def render(assigns) do
    ~H"""
    <button phx-click="start">start</button>
    <div><%= @status %></div>
    """
  end

  # ボタンの phx-click="start" のイベントハンドラ
  def handle_event("start", _, socket) do
    task = Task.async(fn ->
      # (時間のかかる処理の代わり)
      Process.sleep(100)
      123
    end)

    {:noreply, assign(socket, status: "starting", ref: task.ref)}
  end

  # タスクの結果を受け取る
  # (socket に格納したリファレンスと受信したメッセージに含まれるリファレンスが一致をチェックする)
  def handle_info({ref, _result}, socket) when socket.assigns.ref == ref do
    {:noreply, assign(socket, status: "running")}
  end

  # :DOWN のメッセージ等を無視する
  def handle_info(_, socket) do
    {:noreply, socket}
  end
end

aws-sdkで取得できるタグを扱いやすくするための覚書

動機

AWS のリソースの多くは key-value の組みをタグとして設定できるようになっているのですが。

例えば EC2 インスタンスを取得する aws-sdk のメソッド Aws::EC2::Client#describe_instances のレスポンスは次のようになっています。

resp.reservations[0].instances[0].tags #=> Array
resp.reservations[0].instances[0].tags[0].key #=> String
resp.reservations[0].instances[0].tags[0].value #=> String

docs.aws.amazon.com

つまり辞書形式になっているわけではなく、単に key と value のペアを格納するクラスの配列にすぎません。

実装を紐解いてみると、次のような構造になっていました。

class Tag < Struct.new(:key, :value)
end

key から value を引きたい場合、たとえば次のようなコードを書くことになります。

tags = [
  Tag.new("foo", 123),
  Tag.new("bar", 456),
  Tag.new("baz", 789)
]

pp tags.find { |tag| tag.key == "foo" }&.value  # => 123
pp tags.find { |tag| tag.key == "bar" }&.value  # => 456
pp tags.find { |tag| tag.key == "baz" }&.value  # => 789
pp tags.find { |tag| tag.key == "hoge" }&.value # => nil

これがもう少しどうにかならないか、というのが今回のお題です。

クラスで包む

Tag の配列を内部に持ち、key-value アクセスを容易にするメソッドを用意するパタンです。

class Tags
  def self.[](tags)
    new(tags)
  end

  def initialize(tags)
    @tags = tags
  end

  def [](key)
    @tags.find { |tag| tag.key == key }&.value
  end
end

pp Tags[tags]["foo"]  # => 123
pp Tags[tags]["bar"]  # => 456
pp Tags[tags]["baz"]  # => 789
pp Tags[tags]["hoge"] # => nil

これは C++ のときに割と好んで利用していた方法です。

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

struct Tag {
    std::string key;
    int value;
};

class Tags {
public:
    Tags(const std::vector<Tag>& tags) : tags_(tags) {}

    int operator [] (const std::string& key) {
        auto tag = std::find_if(tags_.begin(), tags_.end(), [&](const Tag& tag) { return tag.key == key; });
        return tag->value;
    }

private:
    const std::vector<Tag>& tags_;
};

int main(int, char*[]) {
    auto tags = {
        Tag { "foo", 123 },
        Tag { "bar", 456 },
        Tag { "baz", 789 }
    };

    std::cout << Tags(tags)["foo"] << std::endl;
    std::cout << Tags(tags)["bar"] << std::endl;
    std::cout << Tags(tags)["baz"] << std::endl;
}

これは、動的にクラスのメソッドを変更できない環境ではよくある方法と思いますが、Ruby では個々のオブジェクトにも固有のメソッドを追加できることを利用して、tags オブジェクトにメソッドを追加してみようと思います。

オブジェクトを拡張する (1)

今回はメソッドを定義する Tags をモジュールとして定義し、Object#extend を利用して tags オブジェクトにメソッドを組み込んでいます。

module Tags
  def [](key)
    find { |tag| tag.key == key }&.value
  end
end

tags.extend(Tags)

pp tags["foo"]  # => 123
pp tags["bar"]  # => 456
pp tags["baz"]  # => 789
pp tags["hoge"] # => nil

ただし #[] を上書きしてしまうため Array として機能しなくなるというかなり重度な欠点を抱えます。 Array と被らない名前を選べばよい話ではあるのですが。

pp tags[0] # => nil
pp tags[1] # => nil
pp tags[2] # => nil

オブジェクトを拡張する (2)

より Ruby ならではの方法といえば、やはり BasicObject#method_missing のオーバーライド。 アクセスできるキーがメソッドとして記述できる識別子に限られるという制約がありますが、オブジェクトの属性のように扱うことができるのでとても強力です。

module Tags
  def method_missing(name)
    key = name.to_s
    find { |tag| tag.key == key }&.value
  end
end

tags.extend(Tags)

pp tags.foo  # => 123
pp tags.bar  # => 456
pp tags.baz  # => 789
pp tags.hoge # => nil

Phoenix LiveView 0.18 の新しい構文の覚書とQRコード

Phoenix LiveView 0.18 の構文をいじっています。

具体的にはこれ。

hexdocs.pm

:if and :for

It is a syntax sugar for <%= if .. do %> and <%= for .. do %> that can be used in regular HTML, function components, and slots.

For example in an HTML tag:

<table id="admin-table" :if={@admin?}>
  <tr :for={user <- @users}>
    <td><%= user.name %>
  </tr>
<table>

この形の構文は、従来の Phoenix でも、ふだん仕事で使っている Ruby on Rails でも出てこない形なので、今でもこれを見るとむずむずする感じがあるのですが、入れ子が浅くなることで、反復する要素を実際の深さのレベルで書けるという点はわかりやすくてよいですね。

と、いうわけで。 LiveView 0.18 でQRコードを表示させてみました。

defmodule MyAppWeb.QrLive do
  use MyAppWeb, :live_view

  def mount(_, _, socket) do
    {:ok, assign(socket, view_box: "", size: 0, cells: [])}
  end

  def render(assigns) do
    ~H"""
    <form phx-change="change"><textarea name="qr[text]" /></form>

    <svg viewBox={@view_box} xmlns="http://www.w3.org/2000/svg" fill="black">
      <rect x="0" y="0" width={@size} height={@size} fill="white" />
      <rect :for={{x, y} <- @cells} x={x} y={y} width="1" height="1" />
    </svg>
    """
  end

  def handle_event("change", %{"qr" => %{"text" => text}}, socket) do
    socket =
      case QRCode.create(text) do
        {:ok, %QRCode.QR{matrix: matrix}} ->
          size = length(matrix)

          cells =
            for {row, y} <- Enum.with_index(matrix),
                {cell, x} <- Enum.with_index(row),
                cell == 1,
                do: {x, y}

          assign(socket, view_box: "0 0 #{size} #{size}", size: size, cells: cells)

        _ ->
          socket
      end

    {:noreply, socket}
  end
end

実行例。

コンソールにQRコードを表示したい

Webアプリケーションを開発しているとき、携帯端末での表示を確認したくなるときがあります。 ブラウザのレスポンシブ・デザイン・モードを利用すれば、デスクトップでも見た目の確認はできますが、やはり手のひらの中でどのように表示されるかを知るには、端末そのものに表示させるのが一番です。

そのようなときにURLを携帯端末に送るため、コンソールにQRコードを表示する簡単なスクリプトを書いてみました。

自分で書かなくても、完成度の高いツールは巷に溢れていると思いますが、今後QRコードをブラウザに表示したり、メールで送信したりする必要に迫られることもないとは言えないので、そのトレーニングの意味合いも込めて。

とは言え、QRコードエンコーディングをすべて書くのは大変なので、そこはパッケージを利用し、表現のところだけ自分で実装しています。

エンコーディングのパッケージは Hex に公開されている qr_code を利用しました。

hex.pm

Elixir 1.12 からは Mix.install/2 が実装されて、パッケージを簡単に利用できるようになりました。 これも、スクリプトを書くハードルを下げてくれた気がします。

# qr.exs

Mix.install([:qr_code])

defmodule QR do
  @white IO.ANSI.light_white_background()
  @black IO.ANSI.black_background()

  def show_as_qr_code(str) do
    IO.puts(str)

    {:ok, %{matrix: matrix}} =
      str
      |> QRCode.create()

    len = length(hd(matrix))

    edge = [@white, String.duplicate("  ", len + 2), @black]

    IO.puts(edge)

    matrix
    |> Enum.each(fn row ->
      IO.write([@white, "  ", @black])

      row
      |> Enum.map(fn
        1 -> [@black, "  "]
        0 -> [@white, "  "]
      end)
      |> IO.write()

      IO.puts([@white, "  ", @black])
    end)

    IO.puts(edge)
  end
end

System.argv()
|> Enum.each(&QR.show_as_qr_code/1)

スクリプトを書いたら elixir コマンドで実行です。

$ elixir qr.exs https://elixir-lang.org

初回だけ、パッケージのインストールが実行された後にQRコードが表示されます。

2回目以降は、すぐに結果を表示してくれるはずです。

Elixirのドキュメントでガードをグルーピングするときの覚書

ドキュメントを生成した時に、defguard で定義するガードをグルーピングするときの設定について、いつも忘れてしまい自分の以前のリポジトリを見返すことがたびたびなので、こちらの覚書として記録しておきます。

ガードと関数をモジュールに記述した場合、

defmodule FizzBuzz do
  defguard is_pos_integer(n) when is_integer(n) and n > 0
  defguard is_fizz(n) when is_pos_integer(n) and rem(n, 3) == 0
  defguard is_buzz(n) when is_pos_integer(n) and rem(n, 5) == 0

  def fizz_buzz(n) when is_fizz(n) and is_buzz(n), do: "Fizz Buzz"
  def fizz_buzz(n) when is_fizz(n), do: "Fizz"
  def fizz_buzz(n) when is_buzz(n), do: "Buzz"
  def fizz_buzz(n) when is_pos_integer(n), do: to_string(n)
end

何も指定せず ExDoc でドキュメントを生成すると、ガードも Functions にまとめられます。

ドキュメントを紐解くと、@doc 属性と :groups_for_functions を指定することで関数を任意のグループにグルーピングできると書かれています。

hexdocs.pm

ドキュメントにはガードへの言及はないのですが、defguard の実装を確認してみると、@doc guard: true のの設定が確認できます。

結論として、mix.exs で次のように :groups_for_functions を指定すると、ガードを独立したグループにグルーピングすることができるようになります。

defmodule FizzBuzz.MixProject do
  use Mix.Project

  def project do
    [
      app: :fizz_buzz,
      version: "0.1.0",
      elixir: "~> 1.13",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      docs: docs() # 追加
    ]
  end

  # ...中略...

  # 追加
  defp docs do
    [
      groups_for_functions: [
        Guards: & &1[:guard]
      ]
    ]
  end
end

Elixirでmutableなバイト列を扱う(Rustの力を借りて)

先日のブログに書いた通り、いつ以来かのセルラオートマトンに手を出しています。 そのときの記事の実装では、更新処理に Rust を利用してはいるものの、世代ごとにimmutableなバイト列を生成していました。

Erlang のアロケータを利用するので、確保したメモリの解放忘れのような心配はありませんが、効率を考えるとあまりうれしくない。

というわけで。 今回は、バイト列は Rust で確保し、関数の呼び出しを介して Elixir からそのバイト列を操作してみよう、という回です。

ハンドルあるいはリファレンス

まず一般的な話として。 一方で確保したリソースを、もう一方で利用するばあい、ハンドルを渡すことが常套手段として考えられます。

今回のケースであれば、Rust でメモリ領域を確保し、その情報をハンドルとして Elixir に渡し、Elixir は受け取ったハンドルとその他のパラメータを引数にして Rust の関数を呼び出す、という手順です。

Rustler にはちょうど、Rust でファイルのオープンと読み込みを実装し、それを Elixir から利用するというサンプルが公開されています。 ファイルハンドルを Rust と Elixir の間でやり取りすることで、Elixir から Rust のファイル操作を利用することができるようになっています。

github.com

ちなみに。 ここまでハンドルという言葉を使いましたが、このような用途の値はElixirではリファンレンス型の値として扱われますので、以下リファレンスという言葉で話を進めることにします。

MutableBinary.NIF モジュール

今回も Rust との接続には Rustler を使います。

hex.pm

コード量はそれほど多いわけではありませんが、Elixir と Rust の両方にまたがって実装するので、設定などのためにファイルの数が増えてしまいます。 コード全体は GitHub に上げましたのでそちらを参照ください。

github.com

GitHubリポジトリは、依存パッケージに直接指定することができます。 もし興味がありましたら、新しくプロジェクトを作成して試してみてください。

# mix.exs

  defp deps do
    [
      {:mutable_binary, github: "mattsan/mutable_binary"}
    ]
  end

以降では、注目するコードを抜粋して見ていきたいと思います。

Rust の実装

Rust の実装から見てゆきましょう。

ファイルは、リポジトリnative/mutable_binary_nif/src/lib.rs です。

構造体定義

まず可変なバイト列を格納する構造体を定義します。

後述する初期化のコードで、マクロを利用して Elixir との間で値をやり取りするために必要な種々のコードを生成する…ようです。 ドキュメントの記述が少ないのと、わたしが Rust を学んでいる途中ということで、深いツッコミは今回のところはご容赦を。

struct MutableBinaryResource {
    pub stream: Mutex<Vec<u8>>,
}

バイト列を生成する

先に定義した構造体を利用してバイト列を生成します。

このとき、構造体の値を rustler::resource::ResourceArc という構造体で包みます。 これは、ドキュメントに「std::sync::Arc のようなもの」とあるように、リファレンスカウンタでリソースを管理する構造体のようです。 Drop トレイトが実装されていてガベージコレクションによって解放されるとのこと。

Elixir で扱える形式に変換できる値には rustler::types::Encoder トレイトが実装されていて、encode で変換できるようになっています。

ここでは Rust のタプルを Elixir のタプルに変換するために利用しています。

#[rustler::nif]
fn new(env: Env, size: usize) -> Term {
    if size > 0 {
        let resource = ResourceArc::new(MutableBinaryResource {
            stream: Mutex::new(vec![0; size]),
        });

        (atoms::ok(), resource).encode(env)
    } else {
        (atoms::error(), atoms::out_of_range()).encode(env)
    }
}

リソースを確保できたばあい、アトム :ok とリソースのタプルを返しますが、これを Elixir では アトム :ok とリファレンスの値のタプルとして受け取ります。 確保したバイト列の操作をするときに、このリファレンスを関数の引数として渡します。

指定した位置のバイト値を読み出す

リファレンスとインデクスを引数に受け取り、インデクスの位置のバイトの値を返します。

Elixir が渡したリファレンスは、Rust では元のリソースの形式である ResourceArc<MutableBinaryResource> として受け取ることになります。

この値をロックして元々のバイト列の構造体を取り出します。

インデクスがバイト列の範囲内のばあいは、アトム :ok とバイト列のインデクスで指定されたバイトの値のタプルを encode して返します。

#[rustler::nif]
fn get(env: Env, resource: ResourceArc<MutableBinaryResource>, index: usize) -> Term {
    let resource_struct = resource.stream.try_lock().unwrap();

    if index < resource_struct.len() {
        (atoms::ok(), resource_struct[index]).encode(env)
    } else {
        (atoms::error(), atoms::out_of_range()).encode(env)
    }
}

書き込む、長さを取得する、バイナリとして取得する

これ以外の操作も、基本的に手順になります。

Elixir 側でリファレンスとして渡される ResourceArc<MutableBinaryResource> を受け取り、ロックして Rust の値として取り出し、操作する。

リポジトリに置いたコードには、get の他に set, length, to_string という関数を定義しています。 これらの詳細はリポジトリを参照してみてください。

初期化する

構造体の定義のところで書いたように、マクロを使って必要なコードを生成します。 そしてそのコードを実装に持つ関数を定義します。

fn load(env: Env, _: Term) -> bool {
    rustler::resource!(MutableBinaryResource, env);
    true
}

この関数は、公開する関数と一緒に NIF を初期化する関数に渡されます。

rustler::init!(
    "Elixir.MutableBinary.NIF",
    [new, length, set, get, to_string],
    load = load
);

これで Rust 側の実装は終わりました。

Elixir の実装

次に Elixir の実装です。

ファイルはリポジトリlib/mutable_binary/nif.ex です。

Rust の実装との接続は Rustler が面倒を見てくれるので、Elixir の実装は多くはありません。 Rustleruse しエントリとなる関数を定義するだけで終わってしまいます。 エントリの関数の定義も、単に NIF をロードできなかったばあいのためにエラーを返すだけのシンプルなものです。

defmodule MutableBinary.NIF do
  use Rustler, otp_app: :mutable_binary, crate: :mutable_binary_nif

  def new(_), do: err()
  def length(_), do: err()
  def get(_, _), do: err()
  def set(_, _, _), do: err()
  def to_string(_), do: err()

  defp err, do: :erlang.nif_error(:nif_not_loaded)
end

実演

簡単なスクリプトを書いて実行してみます。

# sample.exs

{:ok, ref} = MutableBinary.NIF.new(16)

MutableBinary.NIF.length(ref) |> IO.inspect(label: "length")
MutableBinary.NIF.to_string(ref) |> IO.inspect(label: "to_string")

MutableBinary.NIF.get(ref, 0) |> IO.inspect(label: "get [0]")
MutableBinary.NIF.set(ref, 0, 123) |> IO.inspect(label: "set [0] <- 123")
MutableBinary.NIF.get(ref, 0) |> IO.inspect(label: "get [0]")
MutableBinary.NIF.get(ref, 16) |> IO.inspect(label: "get [16]")

MutableBinary.NIF.set(ref, 0, 69)
MutableBinary.NIF.set(ref, 1, 108)
MutableBinary.NIF.set(ref, 2, 105)
MutableBinary.NIF.set(ref, 3, 120)
MutableBinary.NIF.set(ref, 4, 105)
MutableBinary.NIF.set(ref, 5, 114)
MutableBinary.NIF.set(ref, 6, 33)
MutableBinary.NIF.set(ref, 7, 32)
MutableBinary.NIF.set(ref, 8, 38)
MutableBinary.NIF.set(ref, 9, 32)
MutableBinary.NIF.set(ref, 10, 82)
MutableBinary.NIF.set(ref, 11, 117)
MutableBinary.NIF.set(ref, 12, 115)
MutableBinary.NIF.set(ref, 13, 116)
MutableBinary.NIF.set(ref, 14, 33)
MutableBinary.NIF.set(ref, 15, 33)

MutableBinary.NIF.to_string(ref) |> IO.inspect(label: "to_string")

実行に先立って、パッケージの取得とコンパイル

$ mix do deps.get, compile

mix runスクリプトを実行します。

$ mix run sample.exs
length: 16
to_string: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
get [0]: {:ok, 0}
set [0] <- 123: :ok
get [0]: {:ok, 123}
get [16]: {:error, :out_of_range}
to_string: "Elixir! & Rust!!"

何も不思議なところはないはずなのですが、Elixir で mutable な値を利用できるというだけで、ちょっと新鮮な感じがしてくるので不思議です。

ちなみに。 GitHub に上げたコードには、MutableBinary.NIF モジュールを背後に隠して、外部とインタフェースする MutableBinary モジュールを用意しています。

いつか読むはずっと読まない:素数が香り、形がきこえる

ライフゲイム Conway's Game of Life で知られる J.H.コンウェイの著作。

新装改題され昨年末に出版されたもの。 書店で偶然見つけ、何かの縁を感じて購入。