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

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

gen_event でイベントを通知する/イベントをハンドリングする

Logger の backend を書いたときに利用した gen_event について調べたので、その覚書。

gen_event とは

Erlang が標準で提供しているモジュールです。 イベントをハンドリングする仕組みを提供してくれます。

複数のハンドラを登録しておくと、イベントがそれらのハンドラに通知されます。

と、いうわけで。書いてみます。

プロジェクトを用意する

gen_event を試すプロジェクトを用意します。 アプリケーションの起動時に gen_event のプロセスを起動したいので --sup オプションをつけて supervision tree の雛形を生成しておきます。

$ mix new notification --sup

gen_event のプロセスを起動するコードを追加する

lib/notification/application.ex を編集します。 children の内容を編集して gen_event を起動する設定を記述します。

defmodule Notification.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      %{
        id: :gen_event,
        start: {
          :gen_event,
          :start_link,
          [{:local, Notification}]
        }
      }
    ]

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

start の値のタプルは、gen_event のプロセスを起動する関数の呼び出し

:gen_event.start_link({:local, Notification})

を表しています。

iex を起動して、Notification.Supervisor が監視しているプロセスの情報を取得すると、gen_event が起動していることがわかります。

$ iex -S mix
iex(1)> Supervisor.which_children(Notification.Supervisor)
[{:gen_event, #PID<0.136.0>, :worker, [:gen_event]}]

ハンドラを書く

ハンドラのファイル lib/notification/handler.ex を追加して gen_event のコールバックを実装したモジュールを記述していきます。

モジュールのふるまい @behaviour:gen_event を指定します。

defmodule Notification.Handler do
  @behaviour :gen_event
end

この状態でコンパイルすると必要なコールバック関数が実装されていないと警告が表示されます。

$ mix compile
Compiling 1 file (.ex)
warning: function handle_call/2 required by behaviour :gen_event is not implemented (in module Notification.Handler)
  lib/notification/handler.ex:1

warning: function handle_event/2 required by behaviour :gen_event is not implemented (in module Notification.Handler)
  lib/notification/handler.ex:1

warning: function init/1 required by behaviour :gen_event is not implemented (in module Notification.Handler)
  lib/notification/handler.ex:1

Generated notification app

handle_call/2, handle_event/2, init/1 の 3 つの関数が必須なことがわかります。 それ以外には、プロセスの終了時に呼び出される terminate/2 や、 gen_event 以外の要因のメッセージが発生したときに呼び出される handle_info/2 、コードが更新されたときに呼び出される code_change/3 があります。が、今回は最小限で実装します。

defmodule Notification.Handler do
  @behaviour :gen_event

  require Logger

  def init(args) do
    name = get_in(args, [:name])
    Logger.info("#{name} initialized")
    {:ok, %{name: name}}
  end

  def handle_call(request, state) do
    Logger.info("#{state.name} called with #{request}")
    {:ok, {:ok, request}, state}
  end

  def handle_event(event, state) do
    Logger.info("#{state.name} received #{event}")
    {:ok, state}
  end
end

ログを出力するだけの実装です。

ハンドラを登録する

iex でアプリケーションを起動します。

$ iex -S mix
iex(1)>

gen_event のプロセスは Notification という名前ですでに起動しているので、ハンドラを登録してみます。

iex(1)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler1")

22:14:30.726 [info]  Handler1 initialized
:ok

Handler1 という名前をつけたハンドラが登録されました。

ハンドラに通知する

この状態で通知を送ってみます。

iex(2)> :gen_event.notify(Notification, "Hi")
:ok

22:15:24.201 [info]  Handler1 received Hi

:gen_event.notify/2 で通知を送ると、handle_event/2 が呼び出されたことがわかります。

:gen_event.call/3 で呼び出すと、handle_call/2 が呼び出されます。 こちらの呼び出しはハンドラのモジュールを指定する必要があります。 同期呼び出しになるので、handle_call/2 が返した値が :gen_event.call/3 の戻り値になります。

:gen_event.call(Notification, Notification.Handler, "Hi")

22:19:38.486 [info]  Handler1 called with Hi
{:ok, "Hi"}

ハンドラのモジュールの関数を呼び出しているだけのようにも見えますが、ハンドラが登録されていない状態で呼び出すとエラーになります。ハンドラが登録されていないと呼び出せないことがわかります。

$ iex -S mix
iex(1)> :gen_event.call(Notification, Notification.Handler, "Hi")
{:error, :bad_module}

複数のハンドラを登録し通知する

iex を起動しなおして、ハンドラを 3 つ名前を変えて登録してみます。

$ iex -S mix
iex(1)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler1")

22:24:54.356 [info]  Handler1 initialized
:ok
iex(2)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler2")

22:24:56.649 [info]  Handler2 initialized
:ok
iex(3)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler3")

22:24:58.466 [info]  Handler3 initialized
:ok

通知を送ります。

iex(4)> :gen_event.notify(Notification, "Hello!")

22:25:47.489 [info]  Handler3 received Hello!
:ok

22:25:47.489 [info]  Handler2 received Hello!

22:25:47.489 [info]  Handler1 received Hello!

登録した 3 つのハンドラが呼び出されたことがわかります。ハンドラの実行は非同期ですので、ハンドラのログの出力と :gen_event.notify/2 の戻り値の表示が混ざって表示されています。

イベント源のコードを書く

ハンドラの登録や通知を簡単にするためのコードを書きます。

lib/notification.ex を編集して :gen_event の関数の呼び出しを隠す関数を書きます。:gen_event のプロセスはこのファイルで記述するモジュールの名前 Notification で登録しているので、__MODULE__ マクロで指定しています。

defmodule Notification do
  def add_handler(name) do
    :gen_event.add_handler(__MODULE__, Notification.Handler, name: name)
  end

  def notify(event) do
    :gen_event.notify(__MODULE__, event)
  end
end

実行します。

$ iex -S mix
iex(1)> Notification.add_handler("Handler1")

22:30:42.760 [info]  Handler1 initialized
:ok
iex(2)> Notification.add_handler("Handler2")

22:30:44.872 [info]  Handler2 initialized
:ok
iex(3)> Notification.add_handler("Handler3")

22:30:46.296 [info]  Handler3 initialized
:ok
iex(4)> Notification.notify("Hello!")

22:30:59.376 [info]  Handler3 received Hello!
:ok

22:30:59.376 [info]  Handler2 received Hello!

22:30:59.376 [info]  Handler1 received Hello!

いつか読むはずっと読まない:ソラリスの陽のもとに

ポーランド語原典からの翻訳版として 国書刊行会 が 2004 年に刊行した単行本を早川書房 が 2015 年に文庫化したもの。

有名な作品ですが、それまではロシア語訳版の翻訳だったんですね。ようやく手にしました。

組合わせ論の順列

もうちょっと簡単に書けるんじゃないかという気がするのですが、とりあえず忘れないうちに。

defmodule Combinatorics do
  def permutation(list), do: permutation(list, Enum.count(list))
  def permutation(_, 0), do: [[]]
  def permutation(list, n) do
    list
    |> Enum.flat_map(fn elem ->
      list
      |> List.delete(elem)
      |> permutation(n - 1)
      |> Enum.map(&[elem | &1])
    end)
  end
end
$ iex
iex(1)> c "combinatorics.ex"
[Combinatorics]
iex(2)> Combinatorics.permutation([1,2,3,4])
[
  [1, 2, 3, 4],
  [1, 2, 4, 3],
  [1, 3, 2, 4],
  [1, 3, 4, 2],
  [1, 4, 2, 3],
  [1, 4, 3, 2],
  [2, 1, 3, 4],
  [2, 1, 4, 3],
  [2, 3, 1, 4],
  [2, 3, 4, 1],
  [2, 4, 1, 3],
  [2, 4, 3, 1],
  [3, 1, 2, 4],
  [3, 1, 4, 2],
  [3, 2, 1, 4],
  [3, 2, 4, 1],
  [3, 4, 1, 2],
  [3, 4, 2, 1],
  [4, 1, 2, 3],
  [4, 1, 3, 2],
  [4, 2, 1, 3],
  [4, 2, 3, 1],
  [4, 3, 1, 2],
  [4, 3, 2, 1]
]
iex(3)> Combinatorics.permutation('abc')    
['abc', 'acb', 'bac', 'bca', 'cab', 'cba']
iex(4)> Combinatorics.permutation([1, 2, 3])   
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
iex(5)> Combinatorics.permutation([1, 2, 3], 1)
[[1], [2], [3]]
iex(6)> Combinatorics.permutation([1, 2, 3], 2)
[[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
iex(7)> Combinatorics.permutation([1, 2, 3], 3)
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
iex(8)> Combinatorics.permutation([1, 2, 3], 4)
[]

Elixirの構造体をJSON形式でシリアライズしたりデシリアライズしたりする

Programming Elixir でも紹介されている JSON を扱うパッケージ poison を利用して、構造体を文字列にシリアライズしたり、文字列からでシリアライズしたりします。

プロジェクトを用意する

mix new でプロジェクトを作成します。

$ mix new book_shelf
$ cd book_shelf

モジュール BookShelf.Book を追加します。

$ mkdir lib/book_shelf
$ touch lib/book_shelf/book.ex
# lib/book_shelf/book.ex
defmodule BookShelf.Book do
  defstruct [:title, :isbn, :price, :bought_at]
end

モジュール BookShelf を編集します。

# lib/book_shelf.ex
defmodule BookShelf do
  defstruct [:books]
end

確認します。

$ iex -S mix
iex(1)> alias BookShelf.Book
BookShelf.Book
iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]}
%BookShelf{
  books: [
    %BookShelf.Book{
      bought_at: ~N[2018-09-11 00:00:00],
      isbn: "9784163908748",
      price: 1620,
      title: "海洋生命5億年史"
    }
  ]
}

poison で JSON へ変換したり、JSON から変換したりする

JSON を扱うパッケージ poison を利用します。

バージョンを確認します。

$ mix hex.info poison
An incredibly fast, pure Elixir JSON library

Config: {:poison, "~> 4.0"}
Releases: 4.0.1, 4.0.0, 3.1.0, 3.0.0, 2.2.0, 2.1.0, 2.0.1, 2.0.0, ...

mix.exs を編集して、依存パッケージに poison を追加します。

  defp deps do
    [
      {:poison, "~> 4.0"}
    ]
  end

パッケージを取得します。

$ mix deps.get

先ほど確認した構造体をエンコードしてみます。

$ iex -S mix
iex(1)> alias BookShelf.Book
BookShelf.Book
iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]}
%BookShelf{
  books: [
    %BookShelf.Book{
      bought_at: ~N[2018-09-11 00:00:00],
      isbn: "9784163908748",
      price: 1620,
      title: "海洋生命5億年史"
    }
  ]
}
iex(3)> Poison.encode(shelf)
{:ok,
 "{\"books\":[{\"title\":\"海洋生命5億年史\",\"price\":1620,\"isbn\":\"9784163908748\",\"bought_at\":\"2018-09-11T00:00:00\"}]}"}

エンコードした文字列をデコードしてみます。

iex(4)> {:ok, json} = Poison.encode(shelf)
{:ok,
 "{\"books\":[{\"title\":\"海洋生命5億年史\",\"price\":1620,\"isbn\":\"9784163908748\",\"bought_at\":\"2018-09-11T00:00:00\"}]}"}
iex(5)> Poison.decode(json)
{:ok,
 %{
   "books" => [
     %{
       "bought_at" => "2018-09-11T00:00:00",
       "isbn" => "9784163908748",
       "price" => 1620,
       "title" => "海洋生命5億年史"
     }
   ]
 }}

JSON の文字列には構造体の情報が含まれていないので、Poison.encode/2 でデコードするとキーを文字列としたマップが返ります。

デコードする構造体を指定する

Poison.encode/2 の第二引数でオプションの :as で構造体を指定します。

iex(6)> Poison.decode(json, as: %BookShelf{})
{:ok,
 %BookShelf{
   books: [
     %{
       "bought_at" => "2018-09-11T00:00:00",
       "isbn" => "9784163908748",
       "price" => 1620,
       "title" => "海洋生命5億年史"
     }
   ]
 }}

オプションの指定なしの場合は %{"books" => ... } というようにキーが文字列のマップとしてデコードされましたが、オプションで構造体を指定すると %BookShelf{books: ... } というように構造体としてデコードされます。 オプションで指定する構造体を入れ子で指定すると子要素も構造体としてデコードしてくれます。

iex(7)> Poison.decode(json, as: %BookShelf{books: [%Book{}]})
{:ok,
 %BookShelf{
   books: [
     %BookShelf.Book{
       bought_at: "2018-09-11T00:00:00",
       isbn: "9784163908748",
       price: 1620,
       title: "海洋生命5億年史"
     }
   ]
 }}

ここでは alias を設定しているので %Book{} と指定していますが、alias がないばあいは %BookShelf.Book{} と指定する必要があります。

要素を変換する

Kernel で定義されている update_in/2関数(正確にはマクロ)を利用するとマップの構造をしたデータの要素を変換することができます。

iex(1)> data = %{a: %{b: 123}}
%{a: %{b: 123}}
iex(2)> update_in(data, [:a, :b], & &1 * 2)
%{a: %{b: 246}}

同じように update_in/2 を利用して Bookbought_at を変換しようとするとエラーになります。

iex(1)> alias BookShelf.Book                                                                       
BookShelf.Book
iex(2)> book = %Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}
%BookShelf.Book{
  bought_at: ~N[2018-09-11 00:00:00],
  isbn: "9784163908748",
  price: 1620,
  title: "海洋生命5億年史"
}
iex(3)> book = book |> Poison.encode!() |> Poison.decode!(as: %Book{})
%BookShelf.Book{
  bought_at: "2018-09-11T00:00:00",
  isbn: "9784163908748",
  price: 1620,
  title: "海洋生命5億年史"
}
iex(4)> update_in(book, [:bought_at], &NaiveDateTime.from_iso8601!/1)
** (UndefinedFunctionError) function BookShelf.Book.get_and_update/3 is undefined (BookShelf.Book does not implement the Access behaviour)
    (book_shelf) BookShelf.Book.get_and_update(%BookShelf.Book{bought_at: "2018-09-11T00:00:00", isbn: "9784163908748", price: 1620, title: "海洋生命5億年史"}, :bought_at, #Function<19.9473146/1 in Kernel.update_in/3>)
    (elixir) lib/access.ex:370: Access.get_and_update/3
    (elixir) lib/kernel.ex:2136: Kernel.update_in/3

エラーメッセージにあるように Accesssget_and_update/3 を実装する必要があります。

BookShelf.Book を次のように書き換えます。

defmodule BookShelf.Book do
  defstruct [:title, :isbn, :price, :bought_at]

  @behaviour Access

  alias BookShelf.Book

  def get_and_update(%Book{} = book, key, function) do
    {:ok,  value} = Map.fetch(book, key)
    {get_value, new_value} = function.(value)
    {get_value, %{book | key => new_value}}
  end
end

Accesss では 3 つの関数が定義されていて、本当は 3 つとも実装する必要があるのですが、今回は get_and_update/3 のみ実装しています。 起動時に実装されていないと警告が出ますがここでは無視して先に進みます。

$ iex -S mix

warning: function fetch/2 required by behaviour Access is not implemented (in module BookShelf.Book)
  lib/book_shelf/book.ex:1

warning: function pop/2 required by behaviour Access is not implemented (in module BookShelf.Book)
  lib/book_shelf/book.ex:1
iex(1)> alias BookShelf.Book
BookShelf.Book
iex(2)> book = %Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]} |> Poison.encode!() |> Poison.decode!(as: %Book{})
%BookShelf.Book{
  bought_at: "2018-09-11T00:00:00",
  isbn: "9784163908748",
  price: 1620,
  title: "海洋生命5億年史"
}
iex(3)> update_in(book, [:bought_at], &NaiveDateTime.from_iso8601!/1)
%BookShelf.Book{
  bought_at: ~N[2018-09-11 00:00:00],
  isbn: "9784163908748",
  price: 1620,
  title: "海洋生命5億年史"
}

update_in/3NaiveDateTime.from_iso8601!/1 を使って bought_at の値を変換することができました。

BookShelf も同じように get_and_update/3 を実装します。

defmodule BookShelf do
  defstruct [:books]

  @behaviour Access

  def get_and_update(%BookShelf{} = book_shelf, key, function) do
    {:ok,  value} = Map.fetch(book_shelf, key)
    {get_value, new_value} = function.(value)
    {get_value, %{book_shelf | key => new_value}}
  end
end
iex(1)> alias BookShelf.Book
BookShelf.Book
iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]} |> Poison.encode!() |> Poison.decode!(as: %BookShelf{books: [%Book{}]})
%BookShelf{
  books: [
    %BookShelf.Book{
      bought_at: "2018-09-11T00:00:00",
      isbn: "9784163908748",
      price: 1620,
      title: "海洋生命5億年史"
    }
  ]
}

ここで変換したい要素 bought_at の位置を指定するばあいは、

  1. BookShelfbooks の要素
  2. その配列の要素である Book
  3. その Bookbought_at

というように途中に配列の指定が必要になります。

配列のすべての要素を指定するには Access.all/0 関数を利用します。

iex(3)> update_in(shelf, [:books, Access.all(), :bought_at], &NaiveDateTime.from_iso8601!/1)
%BookShelf{
  books: [
    %BookShelf.Book{
      bought_at: ~N[2018-09-11 00:00:00],
      isbn: "9784163908748",
      price: 1620,
      title: "海洋生命5億年史"
    }
  ]
}

関数にまとめる

BookShelfserialize/1de serialize/1 を定義します。

defmodule BookShelf do
  defstruct [:books]

  @behaviour Access

  def get_and_update(%BookShelf{} = book_shelf, key, function) do
    {:ok,  value} = Map.fetch(book_shelf, key)
    {get_value, new_value} = function.(value)
    {get_value, %{book_shelf | key => new_value}}
  end

  def serialize(%BookShelf{} = book_shelf) do
    Poison.encode(book_shelf)
  end

  def deserialize(json) when is_binary(json) do
    {:ok, book_shelf} = Poison.decode(json, as: %BookShelf{books: [%BookShelf.Book{}]})
    {:ok, update_in(book_shelf, [:books, Access.all(), :bought_at], &NaiveDateTime.from_iso8601!/1)}
  end
end
iex(1)> alias BookShelf.Book
BookShelf.Book
iex(2)> shelf = %BookShelf{books: [%Book{title: "海洋生命5億年史", isbn: "9784163908748", price: 1620, bought_at: ~N[2018-09-11 00:00:00]}]} 
%BookShelf{
  books: [
    %BookShelf.Book{
      bought_at: ~N[2018-09-11 00:00:00],
      isbn: "9784163908748",
      price: 1620,
      title: "海洋生命5億年史"
    }
  ]
}

シリアライズ

iex(3)> {:ok, json} = shelf |> BookShelf.serialize()
{:ok,
 "{\"books\":[{\"title\":\"海洋生命5億年史\",\"price\":1620,\"isbn\":\"9784163908748\",\"bought_at\":\"2018-09-11T00:00:00\"}]}"}

シリアライズ

iex(4)> json |> BookShelf.deserialize()
{:ok,
 %BookShelf{
   books: [
     %BookShelf.Book{
       bought_at: ~N[2018-09-11 00:00:00],
       isbn: "9784163908748",
       price: 1620,
       title: "海洋生命5億年史"
     }
   ]
 }}

いつか読むはずっと読まない:サメ帝国の逆襲

詳しくは著者のブログの記事を参照。

予想以上に幅広く扱っていて大変満足でした。

海洋生命5億年史 サメ帝国の逆襲

海洋生命5億年史 サメ帝国の逆襲

Elixir の Plug の使い方の覚書

Elixir の Plug を使って HTTP サーバのモックを作る覚書です。

ドキュメントに詳しく書かれていますので、詳しい話はこちらを参照してください。

プロジェクトを用意する

新しいプロジェクトを作ります。

起動時に自動で Plug のアプリケーションを起動したいので --sup オプションを指定してアプリケーションの雛形を生成しておきます。

$ mix new mock_server --sup
$ cd mock_server

mix.exs を編集して依存する cowboyplug の記述を追加します。詳細は、公開されたパッケージを管理している Hex で確認できます。

この他にもコマンドラインから mix hex.info コマンドを使って調べることもできます。

$ mix hex.info cowboy
Small, fast, modular HTTP server.

Config: {:cowboy, "~> 2.4"}
Releases: 2.4.0, 2.3.0, 2.2.2, 2.2.1, 2.2.0, 2.1.0, 2.0.0, 1.1.2, ...

Licenses: ISC
Links:
  GitHub: https://github.com/ninenines/cowboy
$ mix hex.info plug
A specification and conveniences for composable modules between web applications

Config: {:plug, "~> 1.6"}
Releases: 1.6.2, 1.6.1, 1.6.0, 1.5.1, 1.5.0, 1.5.0-rc.2, 1.5.0-rc.1, 1.5.0-rc.0, ...

Licenses: Apache 2
Links:
  GitHub: https://github.com/elixir-plug/plug

ここでは表示された Config: の内容そのままに mix.exs に記述します。

  defp deps do
    [
      {:cowboy, "~> 2.4"},
      {:plug, "~> 1.6"}
    ]
  end

依存するパッケージの取得とコンパイル

$ mix do deps.get, deps.compile

ハンドラを書く

Plug の説明に倣ってハンドラを書きます。 何がリクエストされてもステータスコード 200 で Hello world を返すコードです。

# lib/mock_server.ex
defmodule MockServer do
  import Plug.Conn

  def init(options) do
    options
  end

  def call(conn, _opts) do
    conn
    |> send_resp(200, "Hello world\n")
  end
end

lib/mock_server/ に作成されているアプリケーションの雛形を編集して、 起動時に Plug のアプリケーションを起動するようにします。

# lib/mock_server/application.ex
defmodule MockServer.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      Plug.Adapters.Cowboy2.child_spec(scheme: :http, plug: MockServer, options: [port: 4000])
    ]

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

サーバを起動する

mix run コマンドでアプリケーションを起動します。コマンドはアプリケーションを起動するだけなので、そのままではアプリケーションが起動したらコマンド自体は終了してしまい一緒にアプリケーションも終了してしまいます。

アプリケーションが起動したあとコマンドが終了しないように --no-halt オプションを指定して実行します。

$ mix run --no-halt
Compiling 2 files (.ex)

最初の実行のときに表示されるメッセージから編集した二つのファイルがコンパイルされたことがわかります。

別のコンソールからリクエストを送ってみます。

$ curl http://localhost:4000
Hello world

応答が返りました。

Ctrl+C で終了します。

リクエストの状況を知る

どのようなリクエストを受けているか知りたいので IO.inspect/1 を挟んでみます。

MockServer.call/2 を編集して、パイプラインの途中に IO.inspect/1 を追加します。

  def call(conn, _opts) do
    conn
    |> IO.inspect()
    |> send_resp(200, "Hello world (#{next_count()})\n")
  end

実行。

$ mix run --no-halt
Compiling 1 file (.ex)

同じように別のコンソールからリクエストを送ると Plug.Conn 構造体の内容が表示されます。

%Plug.Conn{
  adapter: {Plug.Adapters.Cowboy2.Conn, :...},
  assigns: %{},
  before_send: [],
  body_params: %Plug.Conn.Unfetched{aspect: :body_params},
  cookies: %Plug.Conn.Unfetched{aspect: :cookies},
  halted: false,
  host: "localhost",
  method: "GET",
  owner: #PID<0.296.0>,
  params: %Plug.Conn.Unfetched{aspect: :params},
  path_info: [],
  path_params: %{},
  peer: {{127, 0, 0, 1}, 61667},
  port: 4000,
  private: %{},
  query_params: %Plug.Conn.Unfetched{aspect: :query_params},
  query_string: "",
  remote_ip: {127, 0, 0, 1},
  req_cookies: %Plug.Conn.Unfetched{aspect: :cookies},
  req_headers: [
    {"accept", "*/*"},
    {"host", "localhost:4000"},
    {"user-agent", "curl/7.54.0"}
  ],
  request_path: "/",
  resp_body: nil,
  resp_cookies: %{},
  resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
  scheme: :http,
  script_name: [],
  secret_key_base: nil,
  state: :unset,
  status: nil
}

バックエンドで実行する

--detached オプションを指定して elixir コマンドを利用して起動すると、コンソールから切り離して実行することができます。

オプションの内容はコマンドのヘルプで確認できます。

$ elixir --help
Usage: elixir [options] [.exs file] [data]

  -e COMMAND                  Evaluates the given command (*)
  ...
  --detached                  Starts the Erlang VM detached from console
  ...

実行。コマンドの実行後すぐにプロンプトが表示されますが、リクエストを送るとアプリケーションが起動していることがわかります。

$ elixir --detached -S mix run --no-halt
$ curl http://localhost:4000
Hello world

コンソールから切り離されているので、先ほどは表示されていた構造体の内容が今回は表示されません。

Logger を利用してログをファイルに出力する記事を書いていますのでこちらも参照してみてください。

また logger_file_backend などのパッケージが HEX に登録されていますのでこれらを利用できます。

終了は kill コマンドなどを駆使してください。

リモートシェルで状態を知る

remote shell を使うと実行しているノードに接続して状態を確認したり操作したりすることができます。

下準備として、状態を持つようにします。

エージェントとして振る舞う Counter モジュールを追加します。

# lib/mock_server/counter.ex
defmodule MockServer.Counter do
  use Agent

  @name __MODULE__

  def start_link(_) do
    Agent.start_link(fn -> 1 end, name: @name)
  end

  def next_count do
    Agent.get_and_update(@name, &{&1, &1 + 1})
  end
end

MockServer.Counter.start_link/1 で起動すると MockServer.Counter.next_count/0 を呼ぶごとに 1 ずつ大きくなる値を返します。

MockServer.Application.start/2 を編集して Counter モジュールも自動的に起動するようにします。

    children = [
      Plug.Adapters.Cowboy2.child_spec(scheme: :http, plug: DummyServer, options: [port: 4000]),
      MockServer.Counter
    ]

MockServer.call/2 を編集し MockServer.Counter.next_count/0 を利用してリクエストを受けた回数をレスポンスに含めるようにします。

  import MockServer.Counter

  def call(conn, _opts) do
    conn
    |> IO.inspect()
    |> send_resp(200, "Hello world (#{next_count()})\n")
  end

ノードに名前をつけて起動します。

$ elixir --sname foo -S mix run --no-halt

まず、状態を持つようになったことを確認します。

別のコンソールからリクエストを送ると、リクエストを送るたびに数が増えていくのがわかります。

$ curl http://localhost:4000
Hello world (1)
$ curl http://localhost:4000
Hello world (2)
$ curl http://localhost:4000
Hello world (3)

次に別のコンソールから、こちらも名前をつけて iex を起動します。

$ iex --sname bar
iex(bar@emattsan)1>

プロンプトに「コマンドラインで指定した名前 + @ + ホスト名」がノード名として表示されているのがわかります。 ここから foo に接続します。

まず Ctrl+G を押します。user switch command のプロンプトが表示されます。

iex(bar@emattsan)1>
User switch command
 -->

? を入力すると利用できるコマンドが表示されます。

User switch command
 --> ?
  c [nn]            - connect to job
  i [nn]            - interrupt job
  k [nn]            - kill job
  j                 - list all jobs
  s [shell]         - start local shell
  r [node [shell]]  - start remote shell
  q                 - quit erlang
  ? | h             - this message
 -->

リモートシェルを起動するには r を利用します。

接続先のノード foo をホスト名付きで指定します。シェルには Elixir.IEx を指定します。

ノード名とシェル名はアトムで指定する必要がありますが、この user switch command は Erlang の文脈で動いているので @ を含む文字列や大文字から始まる文字列がアトムとして扱われるようにシングルクォートで囲みます。 またデフォルトでは Erlang のシェルが起動するので IEx を明示的に指定します。

 --> r 'foo@emattsan' 'Elixir.IEx'
 -->

j で状態を確認します。1 が最初に起動したシェル、 2 が今回指定したシェルです。 * が表示されているので 2 がデフォルトになっているのがわかります。

 --> j
   1  {erlang,apply,[#Fun<Elixir.IEx.CLI.1.111201631>,[]]}
   2* {'foo@emattsan','Elixir.IEx',start,[]}
 -->

c でノードに接続します。番号を指定していないのでデフォルトが選択されます。

 --> c
Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@emattsan)1>

プロンプトの表示が foo に代わっていることがわかります。

…と、長々と iex を起動してからリモートシェルで接続する手順を書きましたが、--remsh オプションを使うことで起動時に接続先を指定することができます。

$ iex --sname bar --remsh foo@emattsan
Erlang/OTP 21 [erts-10.0.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.7.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(foo@emattsan)1>

先ほどリクエストを送って Hello world (3) が表示された直後であれば、次のようにして状態が 4 になっていることが確認できます。

iex(foo@emattsan)1> Agent.get(MockServer.Counter, & &1)
4

もう一度リクエストを送ってみます。

$ curl http://localhost:4000
Hello world (4)

状態が更新されていることがわかります。

iex(foo@emattsans-MBP)2> Agent.get(DummyServer.Counter, & &1)
5

リモートシェルで状態を変更してみます。

iex(foo@emattsans-MBP)3> Agent.update(DummyServer.Counter, & &1 + 10)
:ok
iex(foo@emattsans-MBP)4> Agent.get(DummyServer.Counter, & &1)
15

リクエストを送ると値が変更されていることがわかります。

$ curl http://localhost:4000
Hello world (15)

いつか読むはずっと読まない:ひみつシリーズ「かはくのひみつ」

国立科学博物館のひみつ

国立科学博物館のひみつ

国立科学博物館のひみつ 地球館探検編

国立科学博物館のひみつ 地球館探検編

互いに依存しない同じ処理を複数実行するには Task.Supervisor.async_stream が便利だという覚書

複数のデータに対して、同じ処理を適用する場合、Task.Supervisor.async_stream を利用すると、個々の処理を並列で実行してくれます。CPU を存分に酷使してくれます。

defmodule Sample do
  require Integer

  @doc """
  Collatz conjecture

  see [Collatz conjecture - Wikipedia](https://en.wikipedia.org/wiki/Collatz_conjecture)
  """
  def collatz(n) when is_integer(n) and n > 0, do: collatz(n, [])
  def collatz(1, acc), do: [1 | acc] |> Enum.reverse() |> Enum.join(",")
  def collatz(n, acc) when Integer.is_odd(n), do: collatz(n * 3 + 1, [n | acc])
  def collatz(n, acc), do: collatz(div(n, 2), [n | acc])

  def run do
    # Task.Supervisor をスタートします
    Task.Supervisor.start_link(name: Task.SampleSupervisor)

    Task.SampleSupervisor
    |> Task.Supervisor.async_stream(1..10, Sample, :collatz, [], orderd: false, timeout: :infinity)
    |> Enum.map(&IO.inspect/1)
  end
end

Task.Supervisor.async_stream はストリームを生成するので、結果を得るには Enum.to_list/1 などを利用して要素を評価する必要があります。ここでは Enum.map/2 を使って個々の要素を IO.inspect/1 で出力しています。

実行。

 $ mix run -e 'Sample.run()'
{:ok, "1"}
{:ok, "2,1"}
{:ok, "3,10,5,16,8,4,2,1"}
{:ok, "4,2,1"}
{:ok, "5,16,8,4,2,1"}
{:ok, "6,3,10,5,16,8,4,2,1"}
{:ok, "7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1"}
{:ok, "8,4,2,1"}
{:ok, "9,28,14,7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1"}
{:ok, "10,5,16,8,4,2,1"}

個々の結果は、処理が成功した場合は :ok と処理結果のペアがタプルで返ります。

Queue in Elixir

Q Elixir でキューを利用したいときは?

A Erlang に queue モジュールが用意されているので、それが使えます。

1..10
|> Enum.reduce(
  :queue.new(),                          # 初期値として空のキューを用意
  fn i, q -> :queue.in(i, q) end)        # キューに値を追加する
|> Stream.unfold(fn q ->
  case :queue.out(q) do                  # キューから値を取り出す
    {{:value, i}, next_q} -> {i, next_q} #   キューに値があった場合
    {:empty, _} -> nil                   #   キューが空だった場合
  end
end) |> Enum.to_list()
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

詳細はリファレンスを参照。

Elixir で each_cons を書く

Elixir でコードを書いていて、RubyEnumerable#each_cons に相当する関数が欲しかったのですが、ライブラリに見当たらなかったので自前で書いて見ました。

結論として、Elixir の Stream モジュールに展開関数 Stream.unfold/2 が用意されていたので、それを利用して実装しました。

defmodule MyStream
  def each_cons(seq, n) when is_integer(n) and n > 0 do
    Stream.unfold(seq, fn seq ->
      subseq = Enum.take(seq, n)
      case length(subseq) do
        ^n ->
          {subseq, Stream.drop(seq, 1)}
        _ ->
          nil
      end
    end)
  end
end

実行。

iex> MyStream.each_cons([:a, :b, :c, :d], 2)
#Function<64.58052446/2 in Stream.unfold/2>

Stream モジュールの関数は遅延評価なので、結果を得るには評価してやらないとなりません。

iex> MyStream.each_cons([:a, :b, :c, :d], 2) |> Enum.to_list()
[[:a, :b], [:b, :c], [:c, :d]]
iex> MyStream.each_cons([:a, :b, :c, :d], 3) |> Enum.to_list()
[[:a, :b, :c], [:b, :c, :d]]

Range を与えることもできます。

iex> MyStream.each_cons(?A..?J, 3) |> Enum.to_list()
['ABC', 'BCD', 'CDE', 'DEF', 'EFG', 'FGH', 'GHI', 'HIJ']

遅延評価ということで、具体的な値を得るために Enum.to_list/1 などで評価する必要がありますが、一方で遅延評価なので終端のない列を与えることもできます。

例として、 1 から始まり 1 ずつ増える列を作ります。これも Stream.unfold/2 で作れ ます。

iex> seq = Stream.unfold(1, &{&1, &1 + 1})
#Function<64.58052446/2 in Stream.unfold/2>
iex> seq |> Enum.take(10)
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Stream.unfold/2 の第二引数に与える関数が nil を返すと列の生成を停止しますが、単純に nil を返さない関数を与えることで無限列が生成するできます。

これを踏まえて。

iex> MyStream.each_cons(seq, 4) |> Enum.take(5)
[[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8]]

無限列なので Enum.take/2 を使って必要な数の要素だけ取得しています。

だいたいそんな感じで。