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

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

Rustlerを使ってElixirのNIFを書たときに異なる型の要素を含むリストをあつかいたいばあいの覚書

結論: Vec<Term<'a>> 型にデコードするとよい

リスト全体は Vec<T> 型で受けるとして、T になにを書けばよいのかしばし悩んだのですが、Erlant Term のままあつかうのでよければ Term<'a> 型で受ければよいということに気がつきました。

fn count<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let list: Vec<Term<'a>> = args[0].decode()?;

    Ok(list.len().encode(env))
}

各要素の具体的な値を利用したいばあいは、おのおの必要になった時点でデコードすればよさそうです。

実例

結論を書いてしまったので、以下は Rustler を使う手順の一般的な覚書です。

Rustler を導入する

Rustler は Rust で NIF を書くための便利なライブラリです。 上に書いたコードのように、とても簡単に Rust と Elixir をインタフェースしてくれます。

hex.pm

プロジェクトを作成し、依存するパッケージに Rustler を追加します。

$ mix new my_app
$ cd my_app
  defp deps do
    [
      {:rustler, "~> 0.21"}
    ]
  end

パッケージを導入すると mix rustler.new コマンドが利用できるようになります。

$ mix do deps.get, deps.compile
$ mix help
...
mix rustler.new         # Creates a new Rustler project
...

NIF の雛形を生成する

追加された mix rustler.new コマンドを使っって NIF の雛形を生成します。

モジュール名を訊かれるので入力します。

続けてライブラリ名を訊かれます。 モジュール名から生成した名前が示されるので、その名前でよければそのまま決定します。

native という名前のディレクトリの下に指定したライブラリ名でパッケージが作成されます。

$ mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > MyApp.List
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (myapp_list) >
* creating native/myapp_list/.cargo/config
* creating native/myapp_list/README.md
* creating native/myapp_list/Cargo.toml
* creating native/myapp_list/src/lib.rs

Rust のコードを編集する

native/myapp_list/src/lib.rs を編集します。 ここではリストを受け取って要素の数を返す関数を書きます。

要素の数だけを知りたいので、今回は Erlang Term のままであつかい個々の要素はデコードしません。 このため先に書いたとおり、受け取った引数のリストを Vec<Term<'a>> という型にデコードします。

use rustler::{Encoder, Env, Error, Term};

fn count<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let list: Vec<Term<'a>> = args[0].decode()?;

    Ok(list.len().encode(env))
}

rustler::rustler_export_nifs! {
    "Elixir.MyApp.List",
    [
        ("count", 1, count)
    ],
    None
}

NIF をマウントする Elixir のモジュールを作成する

Rust のコードに書かれたモジュール名の Elixir のモジュールを作成します。

モジュールでは Rustler を use し、オプションで OTP アプリケーション名と、Rust のライブラリ名を指定します。

defmodule MyApp.List do
  use Rustler, otp_app: :my_app, crate: :myapp_list

  def count(_list), do: :erlang.nif_error(:nif_not_loaded)
end

コンパイラとパッケージを指定する

mix.exs を編集してコンパイラに Rustler を追加し、利用するパッケージの指定を追加します。

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      compilers: [:rustler | Mix.compilers()], # 追加 - コンパイラに Rustler を追加する
      rustler_crates: [myapp_list: []],        # 追加 - myapp_list を利用するパッケージに追加する
      deps: deps()
    ]
  end

実行

実行します。 コンパイラに Rustler を追加しているので、Elixir のコードがコンパイルされるのと一緒に Rust のパッケージもコンパイルされます。

$ mix run -e 'MyApp.List.count([1, :ok, {}, [], "ABC"]) |> IO.inspect()'
Compiling NIF crate :myapp_list (native/myapp_list)...
    Finished release [optimized] target(s) in 0.19s
Compiling 2 files (.ex)
Generated my_app app
5

いつか読むはずっと読まない:ラスト・モンスターに泣かされるレベル1パーティー

なけなしのお金で購入した金属製のアイテムをことごとく錆びつかせるラスト・モンスターが、キャリオン・クロウラーと並んで低レベルパーティーの憎き敵役だったのもよい思い出。

ダンジョンズ&ドラゴンズ スターター・セット第5版

ダンジョンズ&ドラゴンズ スターター・セット第5版

  • 発売日: 2017/12/18
  • メディア: おもちゃ&ホビー

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]

Phoenix LiveView で temporary_assigns を設定したときにどのように DOM を削除するか

動作確認に利用したコードは GitHub に push しました。 この記事で解説しているコードは このコミット にまとまっています。

temporary_assigns を設定したときの削除問題

LiveView では、一部が更新されたときに全体を再描画するのではなく、更新された部分だけを再描画するように設定することができます。

hexdocs.pm

ただし可能なのは、新規の DOM の追加と既存の DOM の更新だけで、削除はできません。 削除するには、該当する DOM を削除した全体をサーバからクライアントに送り、再描画する必要があります。 要素の数が少なければそのような方法も使えますが、数が多くなると送信のコストも再描画のコストも無視できなくなります。

これは Elixir Forum でも質問にあがっていた内容です。

elixirforum.com

解決策としては、Forum のこの質問にも回答されているように、JavaScript のフックを使って削除されたことの情報を持っている DOM を削除するというのが現実的なようです。

hexdocs.pm

と、いうわけで。 具体的に実装してみました。

実装のアイディア

アイディアはいたって単純です。

削除したい要素の DOM を更新して、削除の情報を設定します。 DOM の更新後に呼び出されるフック関数で、削除の情報が設定された DOM を削除します。

前提として。

ここでは Ecto ででデータベースを利用していることを想定しています。 ですが、対応するデータが削除されたことが DOM に反映されるようになっていれば、Ecto でなくてもデータベースのデータでなくても、同じように処理できます。

Ecto.Schema の状態を調べる

Ecto.Schema には :__meta__ という名前でメタデータが格納され、そこには :state という名前で状態が格納されています。

hexdocs.pm hexdocs.pm

メタデータの値を取得する Ecto.get_meta/2 が用意されているので、これで状態を確認できます。

Hoge というプロジェクトの Foos というコンテクストに Bar というスキーマが設定され bars というテーブルがあるような状況で、状態を確認してみます。

iex(1)> {:ok, bar} = Hoge.Foos.create_bar(%{name: "hoge"})
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:loaded, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(2)> Ecto.get_meta(bar, :state)
:loaded
iex(3)> {:ok, bar} = Hoge.Foos.get_bar!(1) |> Hoge.Foos.delete_bar()
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:deleted, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(4)> Ecto.get_meta(bar, :state)                                  
:deleted

state の値は次の 3 種類です。

状態
:built データはメモリ上に構築されているがデータベースに保存されていない状態
:loaded データはデータベースに保存されている状態
:deleted データはデータベースから削除されている状態

DOM にスキーマの状態を持たせる

これを利用して。 例えば data-state という属性を追加して、DOM にスキーマの状態を追加します。 また phx-hook でフックを設定します。

<div id="bars" phx-update="append">
  <%= for bar <- @bars do %>
    <div id="bar-<%= bar.id %>" data-state="<%= Ecto.get_meta(bar, :state) %>" phx-hook="Bar">
      <span><%= bar.id %></span>: <span><%= bar.name %></span>
    </div>
  <% end %>
</div>

JavaScript のフックで削除された状態の DOM を削除する

フックでは、DOM が更新された後に呼び出される関数 updated の中で data-state の内容を調べて、"deleted" になっていれば、状態が更新された要素を削除します。

let Hooks = {}

Hooks.Bar = {
  updated() {
    if (this.el.dataset.state == "deleted") {
      this.el.remove()
    }
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})

Hotwire のことを少し

先日 Hotwire が公表されました。 サーバで DOM を構築して送り出すことでページを更新するという、LiveView とも似ている部分があります。

さっそくさわってみました。

気がついたことはいくつかあるのですが、最初に目がいったのは部分的な DOM の削除の箇所。 予想どおりですが、削除のアクションが用意されていて、メソッド呼び出しひとつで指定した DOM を削除できるようになっていました。

www.rubydoc.info

実は LiveView での部分的な DOM の削除は、以前から気になっていた箇所です。 気になりつつも先送りしていたのですが、今回 Hotwire でそれができると知り、がぜん解決したくなったというのが今回の記事を書く動機になりました。

仕事では Ruby on Rails を主戦場にしていることもあり、ゆくゆくは Hotwire を使う機会があるかもしれません。 JavaScript 嫌いマン、JavaScript 書きたくないマン であるわたしにとっては LiveView や Hotwire のようなフレームワークが普及することを願って止みません。

Hotwire については。もう少し使ってみてから記事にしようと思います。

いつか読むはずっと読まない:YOUはSHOCK

日々仕事でデータベースを扱っていて。 SQL以外にもデータをうまくあつかえる手段を手に入れたい、という思いが高まり。 一度は R に手を出したものの挫折。

構文が肌に合わなかったのかもしれないと、Julia に手を出し。 現在挫折中。

構文云々ではなく、まずなにより「データをあつかう」という領域のメンタルモデルが必要なのかもしれない、と感じる今日このごろ。

1から始める Juliaプログラミング

1から始める Juliaプログラミング

そんなわけで。 みなさん、よいお年を。

覚書 join.h

#ifndef __JOIN_H__
#define __JOIN_H__

#include <sstream>
#include <iterator>

template<class Iterator>
std::string join(Iterator begin, Iterator end) {
    std::ostringstream io;

    for(Iterator i = begin; i != end; ++i) {
        io << *i;
    }

    return io.str();
}

template<class Iterator, class Sep>
std::string join(Iterator i, Iterator end, const Sep& sep) {
    std::ostringstream io;

    io << *i;

    while(++i != end) {
        io << sep << *i;
    }

    return io.str();
}

#endif// __JOIN_H__

C++のクラスをNIFでElixirにバインドしてみた

C++ のライブラリを Elixir から利用したくなりました。

NIF を使った単純な関数の呼び出しは実装したことがあるのですが、オブジェクトを利用するようなケースは試したことがなかったので、今回調べてみました。

今回のコードではエラーハンドリングなどは省略していますのでご注意ください。 不備がありましたらご指摘いただけるとありがたいです。

またここで実装したファイルは Gist にもアップしていますので合わせてご参照ください。

NIF環境の用意

elixir_make

mix compile コマンドで C++ のコードがいっしょにコンパイルされるように、elixir_make を導入します。

hex.pm

まず依存パッケージとして mix.exselixir_make を追加します。 このパッケージはコンパイル時のみ利用するので [runtime: false] のオプションを指定します。

またコンパイラのリストに :elixir_make を追加して、 mix compile 実行時に合わせて実行されるようにします。

defmodule Counter.MixProject do
  use Mix.Project

  def project do
    [
      app: :counter,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      compilers: [:elixir_make | Mix.compilers()] # 追加
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp deps do
    [
      {:elixir_make, "~> 0.6", runtime: false} # 追加
    ]
  end
end

counter_nif

NIF のコードを C++ で記述します。 ここではまず動作確認のために引数の値をそのまま戻り値として返す identity/1 を実装してみます。

// c_src/counter_nif.cpp

#include <erl_nif.h>

static ERL_NIF_TERM identity(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    return argv[0];
}

static ErlNifFunc nif_funcs[] = {
    {"identity", 1, identity}
};

ERL_NIF_INIT(Elixir.Counter, nif_funcs, nullptr, nullptr, nullptr, nullptr);

Makefile

Makefile を用意します。

変数の意味の詳細については elixir_makeのドキュメント を参照してください。

PREFIX = $(MIX_APP_PATH)/priv
BUILD = $(MIX_APP_PATH)/obj

NIF = $(PREFIX)/counter_nif.so

CFLAGS = -std=c++11 -fpic
LDFLAGS = -lpthread -dynamiclib -undefined dynamic_lookup

ERL_CFLAGS = -I$(ERL_EI_INCLUDE_DIR)
ERL_LDFLAGS = -L$(ERL_EI_LIBDIR) -lei

SRC = $(wildcard c_src/*.cpp)
HEADERS = $(wildcard c_src/*.h)
OBJ = $(SRC:c_src/%.cpp=$(BUILD)/%.o)

all: install

install: $(PREFIX) $(BUILD) $(NIF)

$(OBJ): $(HEADERS) Makefile

$(BUILD)/%.o: c_src/%.cpp
  $(CC) -c $(CFLAGS) $(ERL_CFLAGS) -o $@ $<

$(NIF): $(OBJ)
  $(CC) $(LDFLAGS) $(ERL_LDFLAGS) -o $@ $^

$(PREFIX) $(BUILD):
  mkdir -p $@

clean:
  $(RM) $(NIF) $(OBJ)

.PHONY: all clean install

Counter module

NIF をバインドする Elixir のモジュールを記述します。

defmodule Counter do
  @on_load {:load_nif, 0}
  @compile {:autoload, false}

  def load_nif do
    Application.app_dir(:counter, "priv/counter_nif")
    |> to_charlist()
    |> :erlang.load_nif(0)
  end

  def identity(_term), do: :erlang.nif_error(:nif_not_loaded)
end

動作確認

ここまでの編集でファイル構成はこのようになっています。

├── Makefile
├── README.md
├── c_src/
│   └── counter_nif.cpp
├── lib/
│   └── counter.ex
├── mix.exs
└── test/
    ├── counter_test.exs
    └── test_helper.exs

mix deps.get でパッケージを取得し、 iex -S mix で IEx を起動します。

問題がなければ make が実行され、Makefile に記述した内容で NIF が作成されます。 Counter.identiy/1 を呼び出して NIF が機能していることを確認します。

$ mix deps.get
$ iex -S mix
iex(1)> Counter.identity(123)
123
iex(2)> Counter.identity("abc")
"abc"
iex(3)> Counter.identity(:abc) 
:abc

C++の Counter クラス

C++ のクラスを追加します。

ここでは簡単なカウンタクラスを例として用います。

// c_src/counter.h

#ifndef COUNTER_H_
#define COUNTER_H_

class Counter {
public:
    Counter(int count = 0) : count_(count) {}
    ~Counter() {}

    int up() { return ++count_; }
    int down() { return --count_; }
    void set(int count) { count_ = count; }
    int get() const { return count_; }

private:
    int count_;
};

#endif//COUNTER_H_

リソースの確保と解放

リソースの確保では次の関数を利用します。

また enif_open_resource_type は NIF がロードされたときに呼び出される必要があるので、ロード時に呼び出されるコールバック関数を用意します。

リソースの解放は garbage collection にまかせます。 リソースが解放されるときに呼び出される関数を用意し、 enif_open_resource_type でその関数を設定します。

counter_nif

c_src/counter_nif.cpp を次のように変更します。 ここではリソースを確保されたときと解放されたときの様子がわかるように enif_fprintf を挿入しています。

#include <cstdio>
#include <erl_nif.h>

// 先に定義した Counter クラスの定義ファイル
#include "counter.h"

// リソースタイプを保存する変数
ErlNifResourceType* CounterType;

// リソースが破棄されるときに呼び出される関数
void destruct_counter(ErlNifEnv* caller_env, void* obj) {
    // リソースが解放される前に、必要な破棄の操作をここに記述する
    enif_fprintf(stdout, "destruct %p\n", obj);
}

// NIF がロードされたときに呼び出される関数
int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) {
    // リソースタイプを作成する
    // 第4引数にリソースが破棄されるときに呼び出される関数を指定する
    CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr);

    return 0;
}

// リソースを確保する関数。Elixir から呼び出される
ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    // リソースを確保する
    void* resource = enif_alloc_resource(CounterType, sizeof(Counter));

    // 確保されたリソースに対して、必要な構築の操作をここに記述する
    enif_fprintf(stdout, "construct %p\n", resource);

    // Erlang がリソースを管理するためのハンドルを作成する
    // これで garbage collection の対象になる
    ERL_NIF_TERM handle = enif_make_resource(env, resource);

    // リソースへの参照を手放す
    enif_release_resource(resource);

    // ハンドルを返却する
    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        handle
    );
}

static ErlNifFunc nif_funcs[] = {
    {"create", 1, create}
};

// 第3引数にロード時に呼び出される関数を指定する
ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);

Counter module

Counter を次のように変更します。

defmodule Counter do
  @on_load {:load_nif, 0}
  @compile {:autoload, false}

  def load_nif do
    Application.app_dir(:counter, "priv/counter_nif")
    |> to_charlist()
    |> :erlang.load_nif(0)
  end

  def create(_init_count), do: :erlang.nif_error(:nif_not_loaded)
end

動作確認

IEx から Counter.create/1 を呼び出します。

$ iex -S mix
iex(1)> Counter.create(0)     
construct 0x00007ff929604a78
{:ok, #Reference<0.3631837740.152436741.82065>}

NIF 内に記述したコードによってリソースのポインタが表示されることが確認できます。 またリソースのハンドルは Elixir の環境からは reference として参照されることがわかります。

このリソースは garbage collection によって解放されますが、上記のように実行すると IEx のシェルの環境から参照されているため解放を確認できません。

mix run コマンドで実行して解放を確認します。

$ mix run -e 'Counter.create(0)'
construct 0x00007fb214e033e8
destruct 0x00007fb214e033e8

リソース解放時に呼び出される関数として登録した destruct_counter が実際に呼び出されていることがわかります。

オブジェクトの構築と破棄

メモリの獲得と解放/分離とオブジェクトの構築/破棄を分離して行うための構文、 placement new と placement delete を利用します。

c_src/counter_nif.cpp を次のように変更します。

#include <new>
#include <erl_nif.h>

#include "counter.h"

ErlNifResourceType* CounterType;

void destruct_counter(ErlNifEnv* caller_env, void* obj) {
    Counter* counter = static_cast<Counter*>(obj);

    // placement delete でオブジェクトを破棄する
    counter->~Counter();
}

int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) {
    CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr);

    return 0;
}

ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    int count;
    enif_get_int(env, argv[0], &count);

    void* resource = enif_alloc_resource(CounterType, sizeof(Counter));

    // placement new でオブジェクトを構築する
    new(resource) Counter(count);

    ERL_NIF_TERM handle = enif_make_resource(env, resource);
    enif_release_resource(resource);

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        handle
    );
}

static ErlNifFunc nif_funcs[] = {
    {"create", 1, create}
};

ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);

メンバ関数の呼び出し

あとは構築したオブジェクトに対して操作を行う関数を追加するだけです。

Elixir のコードに渡された reference = リソースのハンドルを再び NIF に返すと、enif_get_resource でリソースのポインタとして受け取ることができます。

    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

もしくは

    Counter* counter;
    enif_get_resource(env, argv[0], CounterType, reinterpret_cast<void**>(&counter));

counter_nif

Counterメンバ関数 up, down, set, get に対応する関数を実装します。

c_src/counter_nif.cpp を次のように変更します。

#include <new>
#include <erl_nif.h>

#include "counter.h"

ErlNifResourceType* CounterType;

void destruct_counter(ErlNifEnv* caller_env, void* obj) {
    Counter* counter = static_cast<Counter*>(obj);
    counter->~Counter();
}

int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) {
    CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr);

    return 0;
}

ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    int count;
    enif_get_int(env, argv[0], &count);

    void* resource = enif_alloc_resource(CounterType, sizeof(Counter));
    new(resource) Counter(count);

    ERL_NIF_TERM handle = enif_make_resource(env, resource);
    enif_release_resource(resource);

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        handle
    );
}

ERL_NIF_TERM up(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int result = counter->up();

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        enif_make_int(env, result)
    );
}

ERL_NIF_TERM down(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int result = counter->down();

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        enif_make_int(env, result)
    );
}

ERL_NIF_TERM set(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int count;
    enif_get_int(env, argv[1], &count);

    counter->set(count);

    return enif_make_atom(env, "ok");
}

ERL_NIF_TERM get(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int result = counter->get();

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        enif_make_int(env, result)
    );
}

static ErlNifFunc nif_funcs[] = {
    {"create", 1, create},
    {"up", 1, up},
    {"down", 1, down},
    {"set", 2, set},
    {"get", 1, get}
};

ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);

Counter module

Counter にも対応する関数を定義します。

defmodule Counter do
  @on_load {:load_nif, 0}
  @compile {:autoload, false}

  def load_nif do
    Application.app_dir(:counter, "priv/counter_nif")
    |> to_charlist()
    |> :erlang.load_nif(0)
  end

  def create(_init_count), do: :erlang.nif_error(:nif_not_loaded)
  def up(_ref), do: :erlang.nif_error(:nif_not_loaded)
  def down(_ref), do: :erlang.nif_error(:nif_not_loaded)
  def set(_ref, _count), do: :erlang.nif_error(:nif_not_loaded)
  def get(_ref), do: :erlang.nif_error(:nif_not_loaded)
end

動作確認

counter $ iex -S mix
iex(1)> {:ok, ref} = Counter.create(123)
{:ok, #Reference<0.2550255970.1501691911.208805>}
iex(2)> Counter.get(ref)
{:ok, 123}
iex(3)> Counter.up(ref); Counter.up(ref); Counter.up(ref)
{:ok, 126}
iex(4)> Counter.get(ref)                                 
{:ok, 126}
iex(5)> Counter.down(ref)                                
{:ok, 125}
iex(6)> Counter.get(ref) 
{:ok, 125}
iex(7)> Counter.set(ref, 321) 
:ok
iex(8)> Counter.get(ref)     
{:ok, 321}

いつか読むはずっと読まない:本を読む

電車での移動中によく本を読むのですが、最近は外出する機会がめっきり減ってしまい、それにともなって本を読む時間も減ってしまいまいました。 外出しないだけ自宅での時間が取れるのだから、その分だけ本を読めるはずなのですが、なかなか思うようにいきません。

Discord Webhook で Elixir からファイルを POST する

人と直接会う機会がめっきり減り、代わってテキスト、音声、動画を問わずチャットを利用する日々が続いています。

そんなチャット環境を少しでも便利にしようと、Discord の Webhook を Elixir から利用する方法を調べていました。

Discord の Webhook の情報

Webhook の情報自体はポータルにまとまっています。

discord.com

メッセージを投下するだけであれば、Webhook の URL を取得して HTTP の POST メソッドをリクエストすればよいので簡単です。

テキストを POST する

テキストのみの場合、JSON エンコードした body を POST するだけです。 本文は content 、ユーザ名は usernameJSON のキーにします。 content は必須です。 そのほかのフィールドは Discord のドキュメントを参照してください。

ここでは HTTP クライアントに HTTPoison を、JSONエンコードには Jason を利用します。

hex.pm

hex.pm

また Webhook の URL は次のように環境変数 WEBHOOK_URL に設定してあることにします。

$ export WEBHOOK_URL=https://discord.com/api/webhooks/****/*******

あとは Webhook の URL 、JSON エンコードした body 、ヘッダにコンテントタイプを指定して POST すれば OK です。

  def post_text do
    HTTPoison.post(
      System.get_env("WEBHOOK_URL"),
      Jason.encode!(%{content: "Hello"}),
      "Content-Type": "application/json"
    )
  end

ファイルを POST する

Discord のドキュメントに次のように注意書きがされています。

This endpoint supports both JSON and form data bodies. It does require multipart/form-data requests instead of the normal JSON request type when uploading files. Make sure you set your Content-Type to multipart/form-data if you're doing that.

ファイルをアップロードするときは JSON でなくて multipart/form-data で POST する必要があるとのこと。

multipart については HTTPoison のドキュメント で触れられているので、それを参考に引数を変更します。

  def post_file do
    HTTPoison.post(
      System.get_env("WEBHOOK_URL"),
      {
        :multipart,
        [
          {"content", "Hello"}, # フィールド content の値
          {
            :file,
            "image.jpg", # 読み込むファイルのファイル名
            {
              "form-data",
              [{"filename", "image.jpg"}], # 送信するファイル名
            },
            [{"Content-Type", "image/jpg"}] # 送信するファイルのコンテントタイプ
          }
        ]
      },
      "Content-Type": "multipart/form-data"
    )
  end

バイナリを POST する

上の例では既存のファイルを POST しましたが、プログラム内で生成したファイルデータを POST したいときなどは次のように変更します。

multipart に :file を指定した場合に自動的にファイルからデータを取得するようになっているようで、こちらの方がより一般的な設定のようです。

  def post_binary do
    HTTPoison.post(
      System.get_env("WEBHOOK_URL"),
      {
        :multipart,
        [
          {"content", "Hello"}, # フィールド content の値
          {
            "file", # フィールド file
            File.read!("image.jpg"), # 送信するバイナリそのもの
            {
              "form-data",
              [{"filename", "image.jpg"}],
            },
            [{"Content-Type", "image/jpg"}]
          }
        ]
      },
      "Content-Type": "multipart/form-data"
    )
  end

multipart については、HTTPoison が利用しているパッケージ hackney のドキュメントに利用できる形式が説明されています。 が、ちょっとわかりにくいかも。

hex.pm

いつか読むはずっと読まない:Stormbringer! Stormbringer!

Mournblade! Mournblade!

Stormbringer! Stormbringer!

Pow で認証したユーザを LiveView で参照するときの覚書

先日書いた phx_gen_auth とおなじく認証のしくみを Phoenix に組みこむためパッケージ。

phx_gen_auth が認証のしくみを実現するコードを生成するライブラリなのに対し(なので phx_gen_auth 自体はアプリケーション内で利用されない)、Pow はアプリケーションの一部として実行時に認証のしくみを提供するのが大きな違い。

hex.pm

結論

独自の authorization plug を実装し、トークンをセッションに登録する。 LiveView でそのトークンを受け取りユーザを取得する。

実例

アプリケーションを用意する

$ mix phx.new my_app --live
$ cd my_app

Pow を追加する

mix.exsdepspow を追加、パッケージを取得しコンパイルする。

defmodule MyApp.MixProject do
  use Mix.Project

  # ...

  def deps do
    # ...
    {:pow, "~> 1.0"}
  end
end
$ mix do deps.get, deps.compile

認証のしくみを用意する

mix pow.install タスクを利用して、認証のしくみに必要なファイルを生成する。 あわせて既存のファイルに対する変更が表示されるので、それにしたがっってファイルを編集する。

なお、Endpoint には後述するようにカスタマイズした独自のモジュールを指定するので、 Endpoint の編集はそこで説明する。

$ mix pow.install
* creating priv/repo/migrations
* creating priv/repo/migrations/20200915121951_create_users.exs
* creating lib/my_app/users
* creating lib/my_app/users/user.ex
Pow has been installed in your phoenix app!

There are three files you'll need to configure first before you can use Pow.

First, append this to `config/config.exs`:

config :my_app, :pow,
  user: MyApp.Users.User,
  repo: MyApp.Repo

Next, add `Pow.Plug.Session` plug to `lib/my_app_web/endpoint.ex` after `plug Plug.Session`:

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # ...

  plug Plug.Session, @session_options
  plug Pow.Plug.Session, otp_app: :my_app
  plug MyAppWeb.Router
end

Last, update `lib/my_app_web/router.ex` with the Pow routes:

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Pow.Phoenix.Router

  # ... pipelines

  scope "/" do
    pipe_through :browser

    pow_routes()
  end

  # ... routes
end

Config

config/config.exs に Pow の設定を追加する。

config :my_app, :pow,
  user: MyApp.Users.User,
  repo: MyApp.Repo

Router

lib/my_app_web/router.ex で定義されるモジュール MyAppWeb.RouterPow.Phoenix.Router を use する。

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  use Pow.Phoenix.Router # 追加

  # ...
end

scope を追加し pow_routes/0 を追加する。 これによって認証に必要な登録やログインのルーティングが追加される。

defmodule MyAppWeb.Router do
  # ...

  scope "/" do
    pipe_through :browser

    pow_routes()
  end

  # ...
end

pipeline :protected を追加する。

defmodule MyAppWeb.Router do
  # ...

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated,
      error_handler: Pow.Phoenix.PlugErrorHandler
  end

  # ...
end

LiveView を含む scope の pipe_through:protected を追加する。 これによって LiveView のリソースにアクセスするにはログインが必要になる。

defmodule MyAppWeb.Router do
  # ...

  scope "/", MyAppWeb do
    pipe_through [:browser, :protected] # :protected を追加


    live "/", PageLive, :index
    live "/user", UserLive # 追加
  end

  # ...
end

Authorization Plug

ドキュメントにある独自の authorization plug を定義する。

ファイルはどこに配置してもよいけれども、ここではモジュール名に合わせて lib/my_app_web/pow/plug.ex に配置する。

ログインのときに呼び出される create/3 で、セッションにユーザ ID にもとづくトークンを記録する。 LiveView ではこのトークンを利用してログインしているユーザを取得する。

defmodule MyAppWeb.Pow.Plug do
  use Pow.Plug.Base

  @session_key :pow_user_token
  @salt "user salt"
  @max_age 86400

  @impl true
  def fetch(conn, _config) do
    conn  = Plug.Conn.fetch_session(conn)
    token = Plug.Conn.get_session(conn, @session_key)

    MyAppWeb.Endpoint
    |> Phoenix.Token.verify(@salt, token, max_age: @max_age)
    |> maybe_load_user(conn)
  end

  defp maybe_load_user({:ok, user_id}, conn), do: {conn, MyApp.Repo.get(MyApp.Users.User, user_id)}
  defp maybe_load_user({:error, _any}, conn), do: {conn, nil}

  @impl true
  def create(conn, user, _config) do
    token = Phoenix.Token.sign(MyAppWeb.Endpoint, @salt, user.id)
    conn  =
      conn
      |> Plug.Conn.fetch_session()
      |> Plug.Conn.put_session(@session_key, token)

    {conn, user}
  end

  @impl true
  def delete(conn, _config) do
    conn
    |> Plug.Conn.fetch_session()
    |> Plug.Conn.delete_session(@session_key)
  end
end

Endpoint

lib/my_app_web/endpoint.ex に上で定義した plug を設定する。

Pow.PlugSession の結果に依存し、RouterPow.Plug の結果に依存するため、この位置に記述する。

defmodule MyAppWeb.Endpoint do
  # ...

  plug Plug.Session, @session_options
  plug MyAppWeb.Pow.Plug, otp_app: :my_app # Session の後、Router の前のこの位置に追加
  plug MyAppWeb.Router
end

LiveView

認証を必要とする LiveView を定義する。

mount/3 の第二引数のセッション情報から、先に記述した plug の create/3 で記録したトークンを取得する。 このトークンからユーザ ID を復元し、ユーザのデータを取得する。

defmodule MyAppWeb.UserLive do
  use MyAppWeb, :live_view

  @salt "user salt"
  @max_age 86400

  def mount(_params, %{"pow_user_token" => token}, socket) do
    {:ok, user_id} = Phoenix.Token.verify(MyAppWeb.Endpoint, @salt, token, max_age: @max_age)
    current_user = MyApp.Repo.get(MyApp.Users.User, user_id)

    {:ok, assign(socket, current_user: current_user)}
  end

  def render(assigns) do
    ~L"""
    <%= @current_user.email %>
    """
  end
end

サーバを起動し動作を確認する

サーバを起動し、追加した LiveView の URL http://localhost:4000/user にアクセスする。

$ iex -S mix phx.server 

認証されていないのでログインページにリダイレクトされる。

ユーザ登録のためにログインページに表示されているリンク Register をクリックする。

表示されるユーザ登録ページでメールアドレスとパスワードと確認用のパスワードを入力する。

ユーザ登録が完了すると、LiveView のページが表示され、登録したメールアドレスが表示される。

いつか読むはずっと読まない: Peytoia nathorsti

石化した古生物を扱う学問であっても、それもまたダイナミックな人間の営みであることをいつも感じます。

アノマロカリス解体新書

アノマロカリス解体新書

  • 作者:土屋 健
  • 発売日: 2020/02/12
  • メディア: 単行本(ソフトカバー)

phx_gen_auth で認証したユーザを LiveView で参照するときの覚書

hex.pm

結論

LiveView モジュールの mount/3 関数の第二引数に "user_token" が設定されているので、phx_gen_auth が生成するモジュールに含まれる関数 get_user_by_session_token/1 を利用してユーザデータを取得する。

実例

アプリケーションを用意する

$ mix phx.new my_app --live
$ cd my_app

phx_gen_auth を追加する

mix.exsdepsphx_gen_auth を追加する。

このパッケージは開発時のみ利用し、また実行時には利用しないので only: :dev および runtime: false を指定する。

{:phx_gen_auth, "~> 0.4", only: :dev, runtime: false}

パッケージを取得しコンパイルする。

$ mix do deps.get, deps.compile

認証の仕組みを用意する

mix task の mix phx.gen.auth が有効になるので、これを利用して認証のリソースとコードを生成する。

$ mix phx.gen.auth Account User users
$ mix do deps.get, ecto.create, ecto.migrate

認証が必要な LiveView を追加する

lib/my_app_web/router.ex を編集して :require_authenticated_user が指定されているスコープに LiveView のルーティングを追加する。

  scope "/", MyAppWeb do
    pipe_through [:browser, :require_authenticated_user]

    live "/user", UserLive

    # ...
  end

lib/my_app_web/live/user_live.ex を作成して LiveView のモジュール MyAppWeb.UserLiveView を記述する。

このとき、mount/3 の第二引数のマップに、ログインしたユーザを識別するトーク"user_token" が設定されているので、mix phx.gen.auth で生成された MyApp.Account.get_user_by_session_token/1 を使ってそのユーザを取得する。

defmodule MyAppWeb.UserLive do
  use MyAppWeb, :live_view

  def mount(_params, session, socket) do
    current_user = MyApp.Account.get_user_by_session_token(session["user_token"])
    {:ok, assign(socket, current_user: current_user)}
  end

  def render(assigns) do
    ~L"""
    <h1><%= @current_user.email %></h1>
    """
  end
end

サーバを起動し動作を確認する

サーバを起動し、追加した LiveView の URL http://localhost:4000/user にアクセスする。

$ iex -S mix phx.server 

認証されていないのでログインページに移動する。

Register リンクをクリックし登録ページに移動する。

ユーザ登録が完了すると、LiveView のページが表示され、登録したメールアドレスが表示される。

いつか読むはずっと読まない:銃・病原菌・鉄

数年前に購入したままになっていた本。 たまたま先日見た番組に著者が出演していて、存在を思い出し読み始めたところ。

今、この時に、読むのが、よい気がしている。