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

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

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 を使って必要な数の要素だけ取得しています。

だいたいそんな感じで。

Elixir の Logger の custom backend を書く

Elixir にはログ出力の機能を提供する Logger モジュールがあります。が、標準ではログはコンソールに出力されます。 これをファイルに出力する方法を調べたのでまとめておきます。

Logger モジュールのドキュメントはこちら。

ここで書いたコードは GitHub にも push しています。

custom backend を書く

Logger モジュールは、アプリケーションがログを記録するための Logger.info/1, Logger.error/1 といった関数と、それらの関数が呼ばれたときに実際にコンソールやファイルに出力する機能を持っています。この実際に出力する部分は backends として Logger モジュール本体から分離されていて、出力先を追加したり差し替えたりできるようになっています。

デフォルトのコンソールへの出力は次の場所で実装されています。

ログをファイルに出力するための custom backend を作成し、Logger モジュールに設定することでファイル出力を実現していきます。

プロジェクトを用意する

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

mix new で作成されたプロジェクトは実行時にデフォルトで Logger アプリケーションを起動するので後の作業が楽ですし。

プロジェクト名、ソースファイル名は自身でつけた名前に読み替えて読み進めてください。

$ mix new logger_sample_backend
$ cd logger_sample_backend

mix.exs ファイルを見ると Logger アプリケーションの起動が設定されているのがわかります。

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

behaviour を設定する

custom backend は :gen_event として振舞う必要があるので

backend として利用するモジュールに @behaviour :gen_event を記述します。

defmodule LoggerSampleBackend 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 Sample)
  lib/logger_sample_backend.ex:1

warning: function handle_event/2 required by behaviour :gen_event is not implemented (in module Sample)
  lib/logger_sample_backend.ex:1

warning: function init/1 required by behaviour :gen_event is not implemented (in module Sample)
  lib/logger_sample_backend.ex:1

Generated logger_sample_backend app

init を書く - 初期化

init/1 を記述します。

引数にはモジュール名を指定します。ここでは init/1 を記述するモジュール自身なので __MODLE__ マクロで指定しています。

戻り値は :ok と backend が利用する任意の情報とからなるタプルです。 ここではログの出力先のファイルパスとログのフォーマットを値にもつ Map を与えています。

  @path "./log/sample.log"
  @format "$date $time [$level] $message"

  def init(__MODULE__) do
    {:ok, %{path: @path, format: @format}}
  end

フォーマットについては Logger.Formetter モジュールのドキュメントを参照してください。

handle_call を書く - 実行時の設定

Logger.configure_backend/2 関数を利用して実行時に backend の設定を変更することができます。変更を受け付けるには handle_call/2 関数を実装する必要があります。

第 1 引数には : configureLogger.configure_backend/2 から渡されるキーワードリストのペアのタプルを受け取ります。 第 2 引数には設定情報を受け取ります。初期化直後であれば init/2 で設定した値が格納されています。

例えば次のように実行した場合、

Logger.configure_backend(LoggerSampleBackend,  path: "./log/error.log)

handle_call/2handle_call({:configure, [path: "./log/error.log]}, state) という形で呼び出されます。

戻り値には次の 3 つの値のタプルを返します。

  1. :ok
  2. Logger.configure_backend/2 の戻り値になる値
  3. 更新した設定情報

ここでは :path:format というキーでファイルパスとログのフォーマットを受け取って情報を更新し、更新された情報を戻り値として返しています。

  def handle_call({:configure, opts}, state) do
    path = Keyword.get(opts, :path, state.path)
    format = Keyword.get(opts, :format, state.format)
    new_state = %{state | path: path, format: format}
    {:ok, {:ok, new_state}, new_state}
  end

handle_event を書く(1) - ログの出力

Logger.info/1Logger.error/1 が呼ばれると handle_event/2 が呼び出されます。ここで実際にファイルにログ情報を出力します。

第 1 引数にはレベル(info や error など)、ログメッセージ、タイムスタンプなどを格納したタプルを受け取ります。 第 2 引数には設定情報を受け取ります。

戻り値には :ok と設定情報のペアのタプルを返します。ここでは設定の変更などはないので引数で受け取った値をそのまま返しています。

受け取った情報とフォーマットから出力するログを生成する部分は Logger.Formatter のドキュメントを参照してください。

  def handle_event({level, _group_leader, {Logger, message, timestamp, metadata}}, state) do
    state.path |> Path.dirname() |> File.mkdir_p()

    log_line =
      Logger.Formatter.format(
        Logger.Formatter.compile(state.format),
        level,
        message,
        timestamp,
        metadata
      )

    File.write(state.path, "#{log_line}\n", [:append])

    {:ok, state}
  end

handle_event を書く(2) - 出力のフラッシュ

Logger.flush/0 関数が呼ばれた時にも handle_event/2 が呼び出されます。

この時は第 1 引数には : flush が与えられます。

バッファリングしていた場合などにそれをフラッシュするための機能のようですが、今回は必要がないので何もしていません。

  def handle_event(:flush, state) do
    {:ok, state}
  end

handle_info を書く - io_reply をハンドルする

動作の詳細を調べられていないのですが :console と一緒に利用した場合、 :io_reply を含むメッセージを受け取るようです。 処理を記述する必要はありませんが関数を定義しておかないとメッセージをハンドリングできないためエラーが発生します。 エラーが発生するとエラーログが出力され、その出力によってメッセージが送られ、そのメッセージをハンドリングできないためエラーが発生し、エラーが発生すると…と止まらくなります。

  def handle_info({:io_reply, _, :ok}, state) do
    {:ok, state}
  end

実行時の backend の設定

実行時、あるいはモジュール内のコードでこの backend の利用を設定するには Logger.add_backend/1 を利用します。

Logger.add_backend(LoggerSampleBackend)

実行すると、init/1init(LoggerSampleBackend) という形で呼び出されます。

Logger.add_backend/1 の引数が LoggerSampleBackend.init/1 の引数として渡されるので、例えば次のようにするとモジュール名以外に情報を渡すこと不可能というわけではありません。

  def init({__MODULE__, path}) do
    ...
  end
Logger.add_backend({LoggerSampleBackend, "./log/info.log"})

config ファイルでの backend の設定

config ファイル config/config.exs でも backend を設定できます。

config :logger の設定に :backends のキーを追加し値に backend のリストを記述します。

config :logger, backends: [LoggerSampleBackend]

backends は複数指定することができるので例えばデフォルトの :console も利用するなら次のように記述します。

config :logger, backends: [:console, LoggerSampleBackend]

また Logger.add_backend/1 の例のように init/1 の引数が記述されていた場合、:backends でも形式を合わせて記述します。

config :logger, backends: [{LoggerSampleBackend, "./log/info.log"}]

他のプロジェクトから利用する

ここでは logger_sample_backend をプロジェクトとして用意しているので、mix.exs の依存パッケージに追加することで利用できます。

  defp deps do
    [
      {:logger_sample_backend, "~> 0.1", github: "mattsan/logger_sample_backend"}
    ]
  end

追加した後は上記と同じように設定を記述するだけで利用できるようになります。

いつか読むはずっと読まない:艦隊集め

なにやら気づくと艦隊をコレクションしていた。

The Lost Fleet

彷徨える艦隊第一巻。このシリーズで battle cruiser という言葉を知りました。ただし巡るのは洋でないため「巡航戦艦」と訳出されています。

彷徨える艦隊 旗艦ドーントレス (ハヤカワ文庫SF)

彷徨える艦隊 旗艦ドーントレス (ハヤカワ文庫SF)

The Lost Fleet: Beyond the Frontier

彷徨える艦隊の新シリーズ。邦訳は前のシリーズから通巻になっています。

彷徨える艦隊〈7〉戦艦ドレッドノート (ハヤカワ文庫SF)

彷徨える艦隊〈7〉戦艦ドレッドノート (ハヤカワ文庫SF)

The Genesis Fleet

彷徨える艦隊の前日譚…数世紀ぐらい。これから読みます。

彷徨える艦隊 ジェネシス 先駆者たち (ハヤカワ文庫SF)

彷徨える艦隊 ジェネシス 先駆者たち (ハヤカワ文庫SF)

Black Fleet Trilogy

原題を確かめようとページを繰って THE BOOK ONE OF THE BLACK FLEET TRILOGY の文字を見つけた時に、思いました。

…またシリーズものに手を出してしまった。

暗黒の艦隊: 駆逐艦〈ブルー・ジャケット〉 (ハヤカワ文庫SF)

暗黒の艦隊: 駆逐艦〈ブルー・ジャケット〉 (ハヤカワ文庫SF)

The Man of War Trilogy

原題を確かめようとページを繰って THE MAN OF WAR TRILOGY BOOK 1 の文字を見つけた時に、思いました。

…またシリーズものに手を出してしまった。

栄光の旗のもとに: ユニオン宇宙軍戦記 (ハヤカワ文庫SF)

栄光の旗のもとに: ユニオン宇宙軍戦記 (ハヤカワ文庫SF)