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

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

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)

FizzBuzz in Elixir

Elixir で簡単に FizzBuzz を実現するパッケージができてしまったのでブログに書いています。

mix new コマンドでプロジェクトを作成したら、mix.exs を編集してパッケージを追加します。

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

mix deps.get でパッケージを取り込んで FizzBuzz モジュール を書きます。

defmodule FizzBuzz do
  use WordGame, fizz: 3, buzz: 5
end

FizzBuzz モジュールに fizz/1 関数と buzz/1 関数が追加されるので、こんな感じで Fizz ったり Buzz ったりできます。

import FizzBuzz
 1 |> fizz() |> buzz() |> IO.puts() # => 1
 3 |> fizz() |> buzz() |> IO.puts() # => Fizz
 5 |> fizz() |> buzz() |> IO.puts() # => Buzz
15 |> fizz() |> buzz() |> IO.puts() # => FizzBuzz

fizz/1buzz/1 を適用する順序を変えると BuzzFizz もできます。

15 |> buzz() |> fizz() |> IO.puts() # => BuzzFizz

関数名にはマルチバイト文字も利用できるので ほげふが も書けます。

defmodule HogeFuga do
  use WordGame, ほげ: 3, ふが: 5
end
import HogeFuga
 1 |> ほげ() |> ふが() |> IO.puts() # => 1
 3 |> ほげ() |> ふが() |> IO.puts() # => ほげ
 5 |> ほげ() |> ふが() |> IO.puts() # => ふが
15 |> ほげ() |> ふが() |> IO.puts() # => ほげふが
15 |> ふが() |> ほげ() |> IO.puts() # => ふがほげ

関数を繰り返し適用することもできます。

15 |> ほげ() |> ほげ() |> IO.puts()  #=> ほげほげ
15 |> ふが() |> ふが() |> IO.puts() # => ふがふが

Hound を使った Phoenix app の integration test

For browser automation and writing integration tests in Elixir.

Hound を使って Phoenix app の integration test を書きました。 ここで書いたコードは GitHub に push してあります。

mattsan/phoenix_integration_test_sample

プロジェクトを用意

Phoenix app のテストなので、Phoenix プロジェクトを用意します。 プロジェクト名はご自由に。

$ mix phx.new sample
$ cd sample

パッケージを追加する

Hound パッケージを追加します。 mix.exs を編集して、hound の行を追加します。テストでのみ利用するので only: :test のオプションを指定しておきます。

       {:phoenix_html, "~> 2.10"},
       {:phoenix_live_reload, "~> 1.0", only: :dev},
       {:gettext, "~> 0.11"},
-      {:cowboy, "~> 1.0"}
+      {:cowboy, "~> 1.0"},
+      {:hound, "~> 1.0", only: :test}
     ]
   end
 end

編集できたらHound パッケージを取得(ダウンロード)します。

$ mix deps.get

設定する

config/test.exs を編集して Hound の設定を追加します。

テスト時にもサーバが起動するようにします。

config :sample, SampleWeb.Endpoint,
   http: [port: 4001],
-  server: false
+  server: true

ドライバを指定します。ここでは PhantomJS を利用する設定をしています。そのほかのドライバを指定するばあいはこちらを参照してください。

+config :hound, driver: "phantomjs"

test/test_helper.exs を編集してテスト開始前に Hound が起動するようにします。

+Application.ensure_all_started(:hound)
 ExUnit.start()

テストを書く

テストを書きます。今回は test/sample_web/ の下に intagration/ を作り、そこにテストファイルをおくようにしました。

例としてホームページのテスト test/sample_web/intagration/page_index_test.exs を書きます。

  • use Hound.Helpers を追加します
  • hound_session() を追加します。この関数を記述しておくとセッションの管理を自動的に行ってくれます
  • navigate_to/2 でページをリクエストします。詳しくはドキュメントを参照してください
  • find_element/3 で要素を取得します。詳しくはドキュメントを参照してください
  • inner_text/1 で要素のテキストを取得します。詳しくはドキュメント(ry
defmodule SampleWeb.PageIndexTest do
  use SampleWeb.ConnCase
  use Hound.Helpers

  hound_session()

  test "home", %{conn: conn} do
    navigate_to(page_path(conn, :index))

    h2 =
      find_element(:tag, "h2")
      |> inner_text()

    assert h2 == "Welcome to Phoenix!"
  end
end

テストを実行する…前に PhantomJS を起動しておく

PhantomJS を利用するので先に起動しておきます。このばあい、グローバルで実行できる状態でインストールされている必要があります(すぐ後でプロジェクトに含めておく方法を書きます)。 Remote WebDriver mode で利用するのでオプションで指定します。

$ phantomjs --wd

mix test でテストを実行します。今回書いたテストだけを実行するばあいはファイル名を指定して実行します。

$ mix test test/sample_web/intagration/page_index_test.exs 

PhantomJS をパッケージに追加する

PhantomJS はアプリケーションとは別に用意してもよいのですが、プロジェクトの中に閉じておきたいので assets に PhantomJS を追加してそれを利用するようにします。

assets/package.json に PhantomJS を追加します。合わせてスクリプトを定義しておいて簡単に実行できるようにしておきます。

 {
   "repository": {},
   "license": "MIT",
   "scripts": {
     "deploy": "brunch build --production",
-    "watch": "brunch watch --stdin"
+    "watch": "brunch watch --stdin",
+    "phantomjs": "./node_modules/phantomjs-prebuilt/bin/phantomjs --wd"
   },
   "dependencies": {
     "phoenix": "file:../deps/phoenix",
     "phoenix_html": "file:../deps/phoenix_html"
   },
   "devDependencies": {
     "babel-brunch": "6.1.1",
     "brunch": "2.10.9",
     "clean-css-brunch": "2.10.0",
+    "phantomjs-prebuilt": "^2.1.16",
     "uglify-js-brunch": "2.10.0"
   }
 }

インストールします。

$ cd assets
$ npm install

実行するには assets/ に移動して npm run で起動します。

$ npm run phantomjs

PhantomJS をテスト実行時に自動的に起動する、そして終了時に停止する

PhantomJS を別途起動する必要がないように、テストヘルパに PhantomJS の起動と停止を書きます。 これで PhantomJS の起動停止を気にする必要がなくなりますが、

  • テストのたびに起動のオーバーヘッドが発生する
  • 複数のテストを同時には実行できない(それぞれが同じポートを利用しようとするので)

などのデメリットもあります。

test/test_helper.exs を次のように編集します。

TestHelper. setup_phantomjs/0 で PhantomJS を起動します。そのなかでテスト終了時に kill コマンドを実行して PhantomJS のプロセスを停止するコードを System.at_exit/1 にかいておきます。 TestHelper.wait_starting/0 は、PhantomJS の起動が完了したときに標準出力に出力する文字列を待つ関数です。

テスト開始前に PhantomJS が起動するように TestHelper. setup_phantomjs/0 呼び出しを追加します。

defmodule TestHelper do
  @phantomjs_path "./assets/node_modules/phantomjs-prebuilt/bin/phantomjs --wd"

  def setup_phantomjs do
    port = Port.open({:spawn, @phantomjs_path}, [:binary])
    {:os_pid, os_pid} = Port.info(port, :os_pid)

    System.at_exit(fn _ ->
      "kill #{os_pid}"
      |> String.to_charlist()
      |> :os.cmd()
    end)

    wait_starting()
  end

  def wait_starting do
    receive do
      {_port, {:data, output}} ->
        if !String.match?(output, ~r/running on port 8910/) do
          IO.puts "Starting PhatomJS failed: #{inspect output}"
          System.halt(1)
        end

      after 1_000 ->
        wait_starting()
    end
  end
end

TestHelper.setup_phantomjs()
Application.ensure_all_started(:hound)
ExUnit.start()

いつか読むはずっと読まない:もののけのみやつこ

初版が発売されたとき、友人と散々プレイしたカードゲーム。

今年は初版発売から 30 年だそうです。

MONSTER MAKER モンスターメーカー

MONSTER MAKER モンスターメーカー

Mix で管理する Elixir プロジェクトでコンパイル対象のパスを追加する

Phoenix のプロジェクトの mix.exs を見れば一目瞭然なのですが、project/0 が返すキーワードリストに、:elixirc_paths をキーにコンパイル対象のパスのリストを値にして追加すると、それらのパスがコンパイル対象になります。

# mix.exs

  def project do
    [
      app: :sample,
      version: "0.1.0",
      elixir: "~> 1.6",
      elixirc_paths: ["lib", "test/support"], # `lib` と `test/support` をコンパイル対象にする
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

Phoenix プロジェクトと同じように書くと、ビルド環境によって対象を切り替えることができます。

# mix.exs

  def project do
    [
      app: :sample,
      version: "0.1.0",
      elixir: "~> 1.6",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end


  def elixirc_paths(:test), do: ["lib", "test/support"] # test 環境の時は `test/support` をコンパイル対象に含める
  def elixirc_paths(_), do: ["lib"]

ドキュメントの中にはなぜか記述が見つからない。

コードを追いかけてみるとデフォルトでは ["lib"] になっているのがわかります。

Phoenix で Bootstrap の JavaScript を使う

最近、個人的には Elixir ばかり書いています。特に現在は Ruby on Rails TutorialPhoenix でなぞるということをやっています。

とはいえ。やはり勝手が違うので思わぬところでつまずいたり。

例えば Phoenixframework では標準で Bootstrap のスタイルを利用することができますが JavaScript はインストールされていないので、JavaScript を必要とする動きのあるコンポーネントを利用することができません。

調査と試行錯誤の結果、それを使えるようになったので、その手順の記録です。

最小限の手順を記録して置きたいので、プロジェクトの作成から順に説明していきます。

プロジェクトの用意

新しい Phoenix プロジェクトを作成します。sample というプロジェクトを作成しその作業ディレクトリに移動します。

$ mix phx.new sample --no-ecto # 今回は DB を使わないので Ecto を外しました
$ cd sample

Bootstrap コンポーネントを使ったページの用意

Bootstrap のコンポーネントを使ったページを用意します。

例えば lib/sample_web/templates/page/index.html.eex を編集して、内容を Bootstrap のモーダルのサンプルコードで置き換えます。

Phoenix Server を起動します。

$ mix phx.server

ブラウザで http://localhost:4000 開くとボタンが一つ表示されます。

f:id:E_Mattsan:20180327090811p:plain

しかし Bootstrap の JavaScript が機能していないので、ボタンを押してもモーダルは表示されません。

Bootstrap のインストール

Bootstrap をインストールします。

プロジェクトのディレクトリの下の assets ディレクトリに移動して npm で Bootstrap をインストールします。現在利用している Phoenixframewok 1.3.2 では Bootstrap3 を利用しているので、バージョンに 3 を指定します。 また Bootstrap が jquery を必要とするので合わせてインストールします。

$ cd assets
$ npm install bootstrap@3
$ npm install jquery

インストールできたら元のディレクトリに移動します。

Bootstrap の設定

インストールしたパッケージを読み込めるようにします。

assets/js/app.js を開いて次の 2 行を追加します。

global.jQuery = require("jquery")
require("bootstrap")

Bootstrap がグローバルな jQuery を参照しているらしく、このようにする必要がありました。もしかするともう少しよい書き方があるのかもしれません。

以上で設定は完了です。 ボタンを押すと今度は無事モーダルが表示されます。

f:id:E_Mattsan:20180327092159p:plain

明日はもう通用しないかも

ちなみに。現在の Phoenix の変化は早いので、明日はもう通用しないかもしれません。 標準で利用できる Bootstrap もひと世代前のものなので近いうちの更新が予想されます。

次の 1.4.0 では brunch を廃して webpack を利用することになりそうですので、その時は Bootstrap 自体が利用されなくなっているかもしれません。