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

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

Ectoを使ってデータベースを操作する (v2.2)

最初に。

昨年 2018 年 6 月に Ecto の使い方について書いていたのですが、書きかけのまま放置しているうちにメジャーバージョンが上がってしまい、最新の 3.0 ではここに書いてある内容が当てはまらなくなるという事態になりました。なにより sqlite_ecto2 アダプタが(その名の通り)Ecto のバージョン 2 向けのため、Ecto バージョン 3 では利用できません。

とはいえ、まだバージョン 2 が利用できなくなったわけでないですし、アダプタの対応などを除けば使い方はあまり変わらないので、覚書として公開しておきます。リンクのみバージョン 2 の最新 2.2.11 のドキュメントを指すように変更しました。

2 と 3 の違いについては 最新のドキュメントGitHubの CHANGELOG.md などを確認してください。

バージョン 3 についてもそのうち書くと思います。たぶん。

Ecto 2.2

Phoenix ではデータベースの操作のために標準で Ecto を利用するので、作成したプロジェクトは Ecto を利用するための設定が済んでいます。これを手作業で設定してみる試みです。

設定までの手順の話になりますのですので、Ecto の利用方法自体はドキュメントを読んでみてください。

印象としては、意外に簡単に利用することができました。が、意外にコードを書いた気分。

ドキュメント

公式ドキュメントです。ここを読めばだいたい使えるようになります。

手順

プロジェクトを作る

mix new で新しいプロジェクトを用意します。Ecto ではデータベースの操作のためのプロセスを起動することになるので、--sup オプションを指定しておきます。

$ mix new my_db --sup
$ cd my_db

依存パッケージに Ecto を追加する

mix.exs に Ecto を追加します。

  defp deps do
    [
      {:ecto, "~> 2.2"}
    ]
  end

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

$ mix do deps.get, deps.compile

Ecto を利用するための各種コマンドが追加されます。 mix ecto で利用できるコマンドの一覧が表示されます。

$ mix ecto
Ecto v2.2.10
A database wrapper and language integrated query for Elixir.

Available tasks:

mix ecto.create        # Creates the repository storage
mix ecto.drop          # Drops the repository storage
mix ecto.dump          # Dumps the repository database structure
mix ecto.gen.migration # Generates a new migration for the repo
mix ecto.gen.repo      # Generates a new repository
mix ecto.load          # Loads previously dumped database structure
mix ecto.migrate       # Runs the repository migrations
mix ecto.migrations    # Displays the repository migration status
mix ecto.rollback      # Rolls back the repository migrations

リポジトリを作成する

mix ecto.gen.repo コマンドでリポジトリを作成します。

$ mix ecto.gen.repo -r MyDb.Repo
* creating lib/my_db
* creating lib/my_db/repo.ex
* updating config/config.exs
Don't forget to add your new repo to your supervision tree
(typically in lib/my_db/application.ex):

    supervisor(MyDb.Repo, [])

And to add it to the list of ecto repositories in your
configuration files (so Ecto tasks work as expected):

    config :my_db,
      ecto_repos: [MyDb.Repo]

lib/my_db/repo.ex が作成され、config/config.exs にデータベースにアクセスするための設定が追加されます。 デフォルトでは PostgreSQL を利用する設定になっています。

config :my_db, MyDb.Repo,
  adapter: Ecto.Adapters.Postgres,
  database: "my_db_repo",
  username: "user",
  password: "pass",
  hostname: "localhost"

作成時のメッセージにあるように、config/config.exs に設定を追加します。

config :my_db,
  ecto_repos: [MyDb.Repo]

アプリケーションが起動した時に Ecto のプロセスも起動するように、 lib/my_db/application.ex を編集して、監視するプロセスに MyDb.Repo を追加します。

  def start(_type, _args) do
    children = [
      MyDb.Repo
    ]

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

アダプタを追加する

データベースを操作するために Ecto 以外にアダプタのパッケージが必要になります。 自動的に追加された設定では PostgreSQL を利用するようになっていますが、ここでは SQLite3 を使ってみます。データベースがファイルに保存されて、プロジェクトのディレクトリで完結するので後始末が楽なので。

Hexsqlite で検索すると、Ecto 用のアダプタとして sqlite_ecto2 がヒットします。これを利用します。

mix.exs を編集します。

  defp deps do
    [
      {:ecto, "~> 2.2"},
      {:sqlite_ecto2, "~> 2.2"}
    ]
  end

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

$ mix deps.get

config/config.exs を編集します。

config :my_db, MyDb.Repo,
  adapter: Sqlite.Ecto2,
  database: "my_db.sqlite3"

テーブルを追加する

mix ecto.gen.migration コマンドを利用して、テーブルを追加するマイグレーションファイルを作成します。

$ mix ecto.gen.migration books
Generated my_db app
* creating priv/repo/migrations
* creating priv/repo/migrations/20180619123954_books.exs

先頭に日付と時刻の数字の並びを持つマイグレーションファイルが作成されますので、ここにマイグレーションを記述します。 マイグレーションの記述方法は Ecto.Migration のドキュメントに記載されています。

ここではテーブル books の作成を記述しました。

defmodule MyDb.Repo.Migrations.Books do
  use Ecto.Migration

  def change do
    create table("books") do
      add :title, :string, null: false
      add :bought_at, :naive_datetime
      add :price, :integer
he
      timestamps()
    end
  end
end

マイグレーションを実行します。

$ mix ecto.migrate

スキーマファイルを作成する

テーブル books を Ecto で操作するためのスキーマファイル lib/my_db/book.ex を作成します。

スキーマの記述方法は Ecto.Schema のドキュメントに記載されています。

defmodule MyDb.Book do
  use Ecto.Schema

  schema "books" do
    field :title, :string
    field :bought_at, :naive_datetime
    field :price, :integer

    timestamps
  end
end

データベースを操作する

iex で操作してみます。

$ iex -S mix

books の全レコードを取得します。

iex(1)> MyDb.Repo.all(MyDb.Book)

21:54:48.558 [debug] QUERY OK source="books" db=41.6ms
SELECT b0."id", b0."title", b0."bought_at", b0."price", b0."inserted_at", b0."updated_at" FROM "books" AS b0 []
[]

まだレコードを作成していないので空のリストが返ります。

レコードを追加してみます。

iex(2)> MyDb.Repo.insert(%MyDb.Book{title: "ネコと鴎の王冠", price: 734, bought_at: ~N[2017-11-22 00:00:00]})

22:03:22.132 [debug] QUERY OK db=99.0ms queue=0.1ms
INSERT INTO "books" ("bought_at","price","title","inserted_at","updated_at") VALUES (?1,?2,?3,?4,?5) ;--RETURNING ON INSERT "books","id" [{{2017, 11, 22}, {0, 0, 0, 0}}, 734, "ネコと鴎の王冠", {{2018, 6, 19}, {13, 3, 22, 32306}}, {{2018, 6, 19}, {13, 3, 22, 32319}}]
{:ok,
 %MyDb.Book{
   __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
   bought_at: ~N[2017-11-22 00:00:00],
   id: 1,
   inserted_at: ~N[2018-06-19 13:03:22.032306],
   price: 734,
   title: "ネコと鴎の王冠",
   updated_at: ~N[2018-06-19 13:03:22.032319]
 }}

ここで日時の指定には ~N sigil を利用してます。

もう一度全レコードを取得してみます。

iex(3)> MyDb.Repo.all(MyDb.Book)

22:05:54.874 [debug] QUERY OK source="books" db=1.1ms decode=0.1ms
SELECT b0."id", b0."title", b0."bought_at", b0."price", b0."inserted_at", b0."updated_at" FROM "books" AS b0 []
[
  %MyDb.Book{
    __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
    bought_at: ~N[2017-11-22 00:00:00.000000],
    id: 1,
    inserted_at: ~N[2018-06-19 13:03:22.032306],
    price: 734,
    title: "ネコと鴎の王冠",
    updated_at: ~N[2018-06-19 13:03:22.032319]
  }
]

レコードをもう一つ登録。

iex(4)> MyDb.Repo.insert(%MyDb.Book{title: "時空のゆりかご", price: 1188, bought_at: ~N[2018-02-28 00:00:00]})

22:19:13.797 [debug] QUERY OK db=35.1ms
INSERT INTO "books" ("bought_at","price","title","inserted_at","updated_at") VALUES (?1,?2,?3,?4,?5) ;--RETURNING ON INSERT "books","id" [{{2018, 2, 28}, {0, 0, 0, 0}}, 1188, "時空のゆりかご", {{2018, 6, 19}, {13, 19, 13, 759710}}, {{2018, 6, 19}, {13, 19, 13, 759722}}]
{:ok,
 %MyDb.Book{
   __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
   bought_at: ~N[2018-02-28 00:00:00],
   id: 2,
   inserted_at: ~N[2018-06-19 13:19:13.759710],
   price: 1188,
   title: "時空のゆりかご",
   updated_at: ~N[2018-06-19 13:19:13.759722]
 }}

全レコードを取得。

iex(5)> MyDb.Repo.all(MyDb.Book)

22:19:43.295 [debug] QUERY OK source="books" db=1.2ms decode=0.1ms
SELECT b0."id", b0."title", b0."bought_at", b0."price", b0."inserted_at", b0."updated_at" FROM "books" AS b0 []
[
  %MyDb.Book{
    __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
    bought_at: ~N[2017-11-22 00:00:00.000000],
    id: 1,
    inserted_at: ~N[2018-06-19 13:03:22.032306],
    price: 734,
    title: "ネコと鴎の王冠",
    updated_at: ~N[2018-06-19 13:03:22.032319]
  },
  %MyDb.Book{
    __meta__: #Ecto.Schema.Metadata<:loaded, "books">,
    bought_at: ~N[2018-02-28 00:00:00.000000],
    id: 2,
    inserted_at: ~N[2018-06-19 13:19:13.759710],
    price: 1188,
    title: "時空のゆりかご",
    updated_at: ~N[2018-06-19 13:19:13.759722]
  }
]

レコード数を確認します。

iex(6)> MyDb.Book |> select([b], count(b.id)) |> MyDb.Repo.one()

22:20:12.655 [debug] QUERY OK source="books" db=1.0ms
SELECT count(b0."id") FROM "books" AS b0 []
2

だいたいこんな感じで。

いつか読むはずっと読まない:キツネと熊は何処

Markdown から EPUB を作る

Markdown から EPUB を生成する方法の備忘録です。

Markdown から EPUB に変換するなら、md2review + Re:VIEW という強力な組み合わせがあります。

…が、今回は gepub という gem を使って生成してみました。 ここにあげたコードは gist.github.com に全体をアップしています。

gem を用意する

今回利用する gem、ライブラリです。

  • redcarpet
    • Markdown から HTML への変換に利用します。
  • nokogiri
    • HTML から XHTML への変換と要素の編集に利用します。
  • securerandom
    • EPUB に設定する identifier の値を生成するために利用します。
  • gepub
    • EPUB の生成に利用します。
require 'redcarpet'
require 'nokogiri'
require 'securerandom'
require 'gepub'

Markdown を分割する

今回はレベル 1 の見出しを各章の始まりとして扱うようにしました。 そのためまず一つの Markdown を章単位に分割していきます。 なおこのコードでは、レベル 1 の見出しがないケースや、レベル 1 の見出しと見出しの間に文章が含まれないようなケースは考慮してません。

Chapter = Struct.new("Chapter", :title, :body)

def split_md_into_chapters(source)
  result = []
  title = nil
  begin
    body, next_title, rest = source.partition(%r{^# .*\n})
    result << Chapter.new(title, "# #{title}\n#{body}") unless body.empty?
    title, source = next_title[2..-2], rest
  end until source.empty?

  result
end

各章を XHTML に変換する

redcarpet を利用して Markdown から HTML に変換し、nokogiri を利用して HTML から XHTML に変換しています。 redcarpet にも XHTML 形式で出力する機能がありますが、EPUB で利用するには厳密に XML の仕様にそっている必要があるようです。 また EPUB のビューアによっては title 要素が含まれていないと正しく表示できないようです。ここではレベル 1 の見出しの内容をタイトルとして設定しています。

def convert_to_xhtml(chapter)
  markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new, autolink: true, tables: true, fenced_code_blocks: true, prettify: true)
  html = Nokogiri::HTML.parse( markdown.render(chapter.body))
  html.title = chapter.title
  html.to_xhtml
end

XHTML をまとめて EPUB で出力する

gepub を利用して EPUB を生成します。 ここで identifier の設定は必須になっていますが空文字列でも生成できました。ただしビューアによっては identifier が空文字列になっていると正しく表示できないようです。

画像ファイルを含めたい場合は book.ordered ブロックの外で book.add_item を利用して追加します。このとき指定するパスはテキスト内から参照しているパスと合わせる必要があります。 また各ファイルを格納するパスは book.add_item("#{index}.xhtml") のように直下に配置してしまうと、ビューアによってはうまく参照できないようで、コードのようにサブディレクトリに配置するのがよさそうです。

book.generate_epubEPUB をファイルに出力しています。 book.generate_epub_stream を利用するとファイルでなくストリームとして結果を取得できます。このとき結果として返るストリームはカーソルが終端に移動したままになっているようなので、seek(0) で先頭に移動してから読み出す必要があります。

def generate_epub_from_markdown(source)
  book = GEPUB::Book.new {|book|
    book.identifier = "md2epub-#{SecureRandom.uuid}"
    book.title      = 'md2epub - Markdown から EPUB を作る'

    book.ordered do
      split_md_into_chapters(source).each_with_index do |chapter, index|
        xhtml = convert_to_xhtml(chapter)

        book.add_item("text/#{index}.xhtml")
            .add_content(StringIO.new(xhtml))
            .toc_text(chapter.title)
      end
    end
  }

  book.generate_epub('md2epub.epub')
end

実行

簡単な Markdownepub に変換してみます。

generate_epub_from_markdown(<<~MARKDWON)
# Markdown を分割する

レベル 1 の見出しを一つの章として扱うように分割します。

# 各章を XHTML に変換する

gem [redcarpet](https://github.com/vmg/redcarpet) と [nokogiri](https://www.nokogiri.org) を利用して markdown から XHTML に変換します。

```ruby
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new, autolink: true, tables: true)
html = Nokogiri::HTML.parse(markdown.render(source))
html.title = title
xhtml = html.to_xhtml
```

# XHTML をまとめて EPUB で出力する

gem [gepub](https://github.com/skoji/gepub) を利用して XHTML から EPUB に変換します。
MARKDWON

生成された EPUB ファイルを Mac 標準の Books で開くとこのように表示されます。

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

もう展示は終わってしまいましたが、とても興味深い内容でした。

企画展「標本づくりの技(ワザ)-職人たちが支える科博-」

へんなものみっけ! (1) (ビッグコミックス)

へんなものみっけ! (1) (ビッグコミックス)

へんなものみっけ! (2) (ビッグコミックス)

へんなものみっけ! (2) (ビッグコミックス)

へんなものみっけ! (3) (ビッグコミックス)

へんなものみっけ! (3) (ビッグコミックス)

偽クフ語解読器をPrologで書く。

偽クフ語とはなにか。

白と黒のとびら」第11章をご参照ください。

本当はコード上も●と○の列で表現したかったのですが、あつかいがめんどうだったので●を 1 に○を 0 に置き換えてコードしています。 処理系はいつものように GNU-Prolog です

解読器

% 偽クフ語解読器
% decode.prolog

% 規則

% 1. S -> U 011 U (U 'ならば' U)
s(LHS, RHS) :-
  append(U1, [0'0, 0'1, 0'1 | U2], LHS),
  u(U1, T_U1),
  u(U2, T_U2),
  append(T_U1, ['ならば' | T_U2], RHS).

% 2. S -> M 01 U (M 'は' U)
s(LHS, RHS) :-
  append(M, [0'0, 0'1 | U], LHS),
  m(M, T_M),
  u(U, T_U),
  append(T_M, ['は' | T_U], RHS).

% 3. U -> M V
u(LHS, RHS) :-
  append(M, V, LHS),
  m(M, T_M),
  v(V, T_V),
  append(T_M, T_V, RHS).

% 4. U -> M 1 V (M 'に' V)
u(LHS, RHS) :-
  append(M, [0'1 | V], LHS),
  m(M, T_M),
  v(V, T_V),
  append(T_M, ['に' | T_V], RHS).

% 5. U -> M 1 M V (M 'に' M V)
u(LHS, RHS) :-
  append(M1, [0'1 | M2V], LHS),
  append(M2, V, M2V),
  m(M1, T_M1),
  m(M2, T_M2),
  v(V, T_V),
  append(T_M2, T_V, T_M2V),
  append(T_M1, ['に' | T_M2V], RHS).

% 6. U -> U 111 U (U 'そして' U)
u(LHS, RHS) :-
  append(U1, [0'1, 0'1, 0'1 | U2], LHS),
  u(U1, T_U1),
  u(U2, T_U2),
  append(T_U1, ['そして' | T_U2], RHS).

% 7. M -> A N
m(LHS, RHS) :-
  append(A, N, LHS),
  a(A, T_A),
  n(N, T_N),
  append(T_A, T_N, RHS).

% 8. M -> N
m(N, T_N) :-
  n(N, T_N).

% 9. M -> M 010 M (M 'か' M)
m(LHS, RHS) :-
  append(M1, [0'0, 0'1, 0'0 | M2], LHS),
  m(M1, T_M1),
  m(M2, T_M2),
  append(T_M1, ['か' | T_M2], RHS).

% 10. V -> 動詞のいずれか
v("1001", ['見た']).
v("1010", ['変わる']).
v("10100", ['変える']).
v("0011", ['昇る']).
v("1100", ['降りる']).
v("1101", ['留まる']).

% 11. N -> 名詞のいずれか
n("11010", ['1月']).
n("101010", ['2月']).
n("111010", ['3月']).
n("1001010", ['4月']).
n("1011010", ['5月']).
n("1101010", ['6月']).
n("00", ['満月']).
n("11", ['新月']).
n("1101", ['時']).
n("11100", ['希望']).
n("000", ['白']).
n("010", ['黄色']).
n("001", ['薄緑色']).
n("111", ['黒']).
n("101", ['紫色']).
n("110", ['青']).
n("00000", ['無']).
n("0111", ['上']).
n("1000", ['下']).
n("1111", ['ここ']).
n("0100", ['人']).

% 12. A -> 形容詞のいずれか
a("00000", ['正しい']).
a("00010", ['白い']).
a("01010", ['黄色い']).
a("00110", ['薄緑色の']).
a("11110", ['黒い']).
a("10110", ['紫色の']).
a("11010", ['青い']).

% 「クレージュ・レザン兄弟遺稿集(下) レザンの詩集」
poem("文 1", "00010001001011010110100111011110011").
poem("文 2", "11010110101011010").
poem("文 3", "10110111001011011110011").
poem("文 4", "110101101101011010").
poem("文 5", "000100001010110111001011011110011").
poem("文 6", "11110111001011101110100111011110011").
poem("文 7", "11010111101011010").
poem("文 8", "001100001011110111001011011110011").
poem("文 9", "00010001001011001110100111100011100").
poem("文10", "110101100101011010").
poem("文11", "000100001000110001001011100011100").
poem("文12", "111101101010110111001011100011100").
poem("文13", "01010001001011011110011").
poem("文14", "1101011101011010").
poem("文15", "101101101000110001001011011110011").
poem("文16", "000001001011100011100").
poem("文17", "110101110101011010").
poem("文18", "00000010001111111101").

% 結果を表示するための述語
show_sentence((Title, T)) :-
  format("~s ~p~n", [Title, T]).

% 解読した結果をすべて表示する
main :-
  findall(
    (Title, T),
    (poem(Title, S), s(S, T)),
    TS
  ),
  maplist(show_sentence, TS).

実行

第12章にあるように、それぞれの文は一意に決定できないので一つの文に対して複数の解読結果を出力しています。

$ gprolog --consult-file decode.prolog --entry-goal main --query-goal halt
文 1 [白い,満月,見た,ならば,黄色,に,変える,そして,上,に,昇る]
文 1 [満月,か,満月,見た,ならば,黄色,に,変える,そして,上,に,昇る]
文 1 [白,に,白,見た,ならば,黄色,に,変える,そして,上,に,昇る]
文 2 [時,は,2月,に,変わる]
文 2 [青い,新月,は,黄色,に,変わる]
文 2 [新月,か,新月,は,黄色,に,変わる]
文 3 [紫色の,新月,見た,ならば,上,に,昇る]
文 4 [時,は,紫色の,紫色,変わる]
文 4 [時,は,5月,に,変わる]
文 5 [満月,か,満月,か,紫色の,新月,見た,ならば,上,に,昇る]
文 5 [白い,満月,か,紫色の,新月,見た,ならば,上,に,昇る]
文 5 [満月,か,満月,か,紫色の,新月,見た,ならば,上,に,昇る]
文 5 [白,に,白,か,紫色の,新月,見た,ならば,上,に,昇る]
文 6 [黒い,新月,見た,ならば,紫色,に,変える,そして,上,に,昇る]
文 7 [時,は,3月,に,変わる]
文 8 [薄緑色の,満月,か,黒い,新月,見た,ならば,上,に,昇る]
文 8 [満月,に,下,か,黒い,新月,見た,ならば,上,に,昇る]
文 8 [薄緑色,に,白,か,黒い,新月,見た,ならば,上,に,昇る]
文 8 [薄緑色の,満月,か,ここ,は,青,か,青,に,黒,昇る]
文 8 [薄緑色の,満月,か,ここ,は,青,か,時,に,新月,昇る]
文 9 [白い,満月,見た,ならば,薄緑色,に,変える,そして,下,に,降りる]
文 9 [白い,満月,見た,ならば,満月,に,青,見た,そして,白,に,降りる]
文 9 [満月,か,満月,見た,ならば,薄緑色,に,変える,そして,下,に,降りる]
文 9 [満月,か,満月,見た,ならば,満月,に,青,見た,そして,白,に,降りる]
文 9 [白,に,白,見た,ならば,薄緑色,に,変える,そして,下,に,降りる]
文 9 [白,に,白,見た,ならば,満月,に,青,見た,そして,白,に,降りる]
文10 [時,は,4月,に,変わる]
文10 [青い,青,は,黄色,に,変わる]
文10 [新月,か,青,は,黄色,に,変わる]
文11 [満月,か,満月,か,薄緑色の,満月,見た,ならば,下,に,降りる]
文11 [白い,満月,か,薄緑色の,満月,見た,ならば,下,に,降りる]
文11 [満月,か,満月,か,薄緑色の,満月,見た,ならば,下,に,降りる]
文11 [白,に,白,か,薄緑色の,満月,見た,ならば,下,に,降りる]
文11 [満月,か,白い,満月,に,下,見た,ならば,下,に,降りる]
文11 [満月,か,満月,か,満月,に,下,見た,ならば,下,に,降りる]
文11 [白い,満月,か,満月,に,下,見た,ならば,下,に,降りる]
文11 [満月,か,満月,か,満月,に,下,見た,ならば,下,に,降りる]
文11 [満月,か,白い,薄緑色,に,白,見た,ならば,下,に,降りる]
文11 [満月,か,満月,か,薄緑色,に,白,見た,ならば,下,に,降りる]
文11 [白い,満月,か,薄緑色,に,白,見た,ならば,下,に,降りる]
文11 [満月,か,満月,か,薄緑色,に,白,見た,ならば,下,に,降りる]
文12 [黒い,新月,か,紫色の,新月,見た,ならば,下,に,降りる]
文12 [黒い,時,か,時,に,見た,ならば,下,に,降りる]
文13 [黄色い,満月,見た,ならば,上,に,昇る]
文13 [黄色,に,白,見た,ならば,上,に,昇る]
文14 [時,は,1月,に,変わる]
文15 [紫色の,新月,か,薄緑色の,満月,見た,ならば,上,に,昇る]
文15 [紫色の,新月,か,満月,に,下,見た,ならば,上,に,昇る]
文15 [紫色の,新月,か,薄緑色,に,白,見た,ならば,上,に,昇る]
文16 [無,見た,ならば,下,に,降りる]
文17 [新月,は,上,か,紫色,変わる]
文17 [時,は,青い,紫色,変わる]
文17 [時,は,新月,か,紫色,変わる]
文17 [時,は,6月,に,変わる]
文17 [青い,黒,は,黄色,に,変わる]
文17 [新月,か,黒,は,黄色,に,変わる]
文18 [正しい,人,は,ここ,に,留まる]
文18 [正しい,人,は,新月,に,新月,留まる]
| ?- halt.

Prolog たのしい。

いつか読むはずっと読まない:0 と 1 の Prolog

書籍のページ で第1章が公開されていますので、興味のある方はどうぞ。

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)

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

国立科学博物館のひみつ

国立科学博物館のひみつ

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

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