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

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

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)

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

国立科学博物館のひみつ

国立科学博物館のひみつ

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

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

互いに依存しない同じ処理を複数実行するには 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 と処理結果のペアがタプルで返ります。