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

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

AWS SNS で SMS 送信したときの料金を調べる覚書

AWS を使って SMS を送信する機会があったので、送信にかかった利用金を CloudWatch Metrics で取得するコードを Ruby で書きました。その覚書。

require 'aws-sdk-cloudwatch'

params = {
  metric_data_queries: [
    {
      id: 'spentUSD',
      metric_stat: {
        metric: {
          namespace: 'AWS/SNS',
          metric_name: 'SMSMonthToDateSpentUSD'
        },
        period: 24 * 60 * 60, # seconds per day
        stat: 'Maximum'
      },
    }
  ],
  start_time: Time.new(2020, 1, 1),
  end_time: Time.new(2020, 4, 1)
}

client = Aws::CloudWatch::Client.new(region: 'ap-northeast-1')

metric_data_results = []

loop do
  response = client.get_metric_data(params)
  metric_data_results.concat(response.metric_data_results)

  break unless response.next_token

  params.next_token = response.next_token
end

pp metric_data_results.flatten

Railsでサーバサイドで動的にHTMLを更新したいための覚書

動機

  • Rails で動的に要素を更新するページを作りたい
  • JavaScript 勢としては、JavaScript のコードをできるだけ書かずにすませたい

今回の解決策のメリット

  • サーバサイドでレンダリングするので、View のテンプレートを利用できる

今回の解決策のデメリット

  • データの転送量が多い

よいですよね、 Phoenix LiveView 。 同じようなことを Rails でもやってみたいと思い立ち、試行錯誤してみました。

app を用意する

サンプルとして、サーバサイドで時刻を更新しブラウザ上で刻々と表示を更新する clock app を作ります。

Rails のバージョンとしては 6.0 、標準で WebPacker を利用していることを前提にしています。

$ rails new clock
$ cd clock

コントローラを追加する

ページを表示するためにコントローラを用意します。 更新は動的に行うのでコントローラのアクションは show のみ用意しています。

$ bin/rails g controlle clocks show

チャネルを追加する

更新する要素を輸送するチャネルを用意します。 クライアントからの update の要求を受け取れるようにイベントハンドラを用意しています。

$ bin/rails g channel clock update

JavaScript を編集する

本音としてはいろいろあっても、やはり JavaScript のコードをまったく書かずにすますわけにはいきませんでした。

と、いうわけで。 自動生成された JavaScript のコードを編集します。

app/javascript/packs/application.js

ページごとに .js ファイルの読み込みを制御したいので、チャネルの .js ファイルを一括で読み込んでいるコードをコメントアウトします。

require("@rails/ujs").start()
require("turbolinks").start()
// require("channels") コメントアウトする

app/javascript/channels/clock_channel.js

consumer.subscriptions.create の行の先頭に export default を追加してチャネルのオブジェクトを export します。

このファイルで定義されている関数を編集してカスタマイズするのが行儀のよい方法なのだと思います。 今回は、サーバから受信したデータをハンドリングする関数をこのオブジェクトの received にあとから代入するという方法をとります。

import consumer from "./consumer"

export default consumer.subscriptions.create("ClockChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    // Called when there's incoming data on the websocket for this channel
  },

  update: function() {
    return this.perform('update');
  }
});

app/javascript/packs/clock.js

export されたチャネルのオブジェクトを import し、各イベントをハンドリングする .js ファイルを用意します。

import clockChannel from '../channels/clock_channel'

window.addEventListener('DOMContentLoaded', () => {
  // ブラウザで update がクリックされたら、チャネルの update を呼び出す
  const buttonUpdate = document.getElementById('update')
  buttonUpdate.addEventListener('click', () => { clockChannel.update() })

  // チャネルからデータを受け取ったら container に格納Gする
  const container = document.getElementById('container')
  clockChannel.received = (data) => { container.innerHTML = data.html }
})

view を編集する

app/views/clocks/show.html.erb

update のイベントを発するボタン、動的に生成された要素を格納するコンテナを配置します。 また先に用意したチャネルの .js ファイルを読み込むタグを追加します。

<button id="update">UPDATE</button>
<div id="container"></div>

<%= javascript_pack_tag 'clock' %>

channel を編集する

app/channels/clock_channel.rb

update メソッドの内容に tramsmit メソッドでデータを送信するコードを追加します。 ここでは現在時刻を文字列に変換し div で囲ったものを送っています。

class ClockChannel < ApplicationCable::Channel
  periodically :update, every: 1.second

  def subscribed
    # stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def update
    transmit(html: "<div>#{Time.current}</div>")
  end
end

表示を確認する

Rails サーバを起動して、追加したページをブラウザで開きます。

ページを開くとボタンが一つ表示されています。 それをクリックすると、現在時刻が表示されるはずです。

更新する要素をテンプレートで書く

更新する要素を update メソッドに直書きするのではサーバサイドで更新するメリットがありません。 view と同じようにテンプレートを使ってレンダリングするようにしてみます。

テンプレートを用意する

app/views/clocks/clock.html.erb

まず一般の view と同じようにテンプレートを用意します。

<div><%= current_time %></div>

app/views/layouts/plain.html.erb

テンプレートのレンダリングではレイアウトが利用されます。 標準で用意されているレイアウトは HTML のヘッダなどが含まれています。 ページ内の要素をレンダリングするにはそれらは不要ですので、何も装飾しないレイアウトを用意します。

<%= yield %>

channel を編集する

app/channels/clock_channel.rb

ApplicationController.render を使って用意したテンプレートでレンダリングします。

update メソッドを次のように変更します。

  def update
    html = ApplicationController.render('clocks/clock.html', layout: 'plain.html', locals: {current_time: Time.current.to_s})
    transmit(html: html)
  end

表示を確認する

再び Rails サーバを起動してページをブラウザで開きます。 編集前と変わらない表示ができています。

view と同じテンプレートを利用できるので SlimHaml を利用することもできます。

定期的に表示を更新する

Action Cable には Periodically というメソッドが用意されています。 これを利用するとクライアントから update のイベントを送ることなく、サーバが自ら定期的に更新を実行できるようになります。

app/channels/clock_channel.rb

ClockChannel クラスに periodically :update, every: 1.second の一行を追加します。

class ClockChannel < ApplicationCable::Channel
  periodically :update, every: 1.second

  # 以下略

表示を確認する

三度 Rails サーバを起動してページをブラウザで開きます。 今度は update ボタンを押さなくても秒ごとに表示が更新されることがわかります。

periodically を設定すると常に update メソッドが呼ばれていないかと気になりますが、チャネルは接続の状態( subscribed / unsubscribed )を把握しているので接続中のみメソッドを呼ぶようになっています。

いつか読むはずっと読まない:Life on Earth

博士は間違いなくわたしの科学好きに影響を与えたお一人です。

Elixir で escript からサーバにリクエストを送る覚書

事の起こり

NIF (Native Implemented Functions) を含むパッケージを escript で利用しようとしたのですが。

ビルドした実行ファイルを実行しても NIF のライブラリファイルを読み込めないとエラーになり。

検索してみたらこのとおり。


Unfortunately that's a limitation of escripts. They cannot access anything in priv and therefore they cannot access embedded .so files. In other words, it is not currently possible to build escripts with NIFs in them.

https://github.com/elixir-lang/elixir/issues/5444#issuecomment-260178836


You can not use NIFs in an escript.

https://elixirforum.com/t/escript-can-not-load-argon2-nif-wrong-approach-for-cli/25399/2


と、いうことなので。 NIF を含むサーバを起動しておいて escript の実行ファイルからプロセス間通信でリクエストを送ればよいじゃないか、と考えたしだい。

サーバを用意する

まずリクエストを受ける適当なサーバを用意します。

$ mix new my_server --sup

モジュールを用意する

適当なサーバモジュールとアプリケーションモジュールを用意します。

defmodule MyServer do
  use GenServer

  @name __MODULE__

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: @name)
  end

  def double(value) do
    GenServer.call(@name, {:double, value})
  end

  def init(_opts) do
    {:ok, :no_state}
  end

  def handle_call({:double, value}, _from, state) do
    {:reply, value * 2, state}
  end
end
defmodule MyServer.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      MyServer
    ]

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

release 設定を用意する

ノード間で通信を行うには、ノードに名前を設定する必要があります。

まず mix release.init コマンドを実行して設定ファイルを生成します。

$ mix release.init
* creating rel/vm.args.eex
* creating rel/env.sh.eex
* creating rel/env.bat.eex

rel/env.sh.eex (MS Windows のばあいは rel/env.bat.eex )を編集し、ファイルの末尾の 2 行のコメントアウトを外します。

この行の直前のコメントに書かれているように、これでノード名が設定されます。 デフォルトではアプリケーション名 + 127.0.0.1 という名前になるので、今回は my_server@127.0.0.1 という名前が設定されます。

export RELEASE_DISTRIBUTION=name
export RELEASE_NODE=<%= @release.name %>@127.0.0.1

cookie を設定する

安全のために cookie を設定します。

明示的に cookie を設定しない場合は自動的にランダムな値が設定されます。 その値を利用しても問題ないのですが、ここでは説明を兼ねて設定します。

mix.exs ファイルを開き、project/0 が返すキーワードリストに :release を追加します。

release には名前をつけて異なる設定で生成することができますが、ここではデフォルトのアプリケーション名に対して cookie の設定をします。

defmodule MyServer.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_server,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      releases: [
        my_server: [
          cookie: "MY_SERVER_COOKIE"
        ]
      ]
    ]
  end

  # 以下略

end

release を作成する

mix release を実行します。

$ mix release

実行に必要なファイルが _build/dev/rel/my_server に生成されます。 今回は MIX_ENV を指定していないのででデフォルトの dev で生成されています。

サーバを起動するにはこのディレクトリに含まれる _build/dev/rel/my_server/bin/my_server を利用します。

動作を確認する

start または daemon コマンドでサーバを起動します。

$ _build/dev/rel/my_server/bin/my_server start

または

$ _build/dev/rel/my_server/bin/my_server daemon

サーバが起動したら remote コマンドでサーバのノードに接続します。 start で起動したばあいは別のコンソールを開いてコマンドを実行してください。

$ _build/dev/rel/my_server/bin/my_server remote
iex(my_server@127.0.0.1)1> MyServer.double(123)
246

動作が確認できたら stop コマンドで停止します。 start で起動したばあいは Ctrl+C でも停止できます。

$ _build/dev/rel/my_server/bin/my_server stop

escript を用意する

次にリクエストを送る適当な escript を用意します

$ mix new my_cli

モジュールを用意する

エントリポイントになる関数 main/1 を定義したモジュールを用意します。

defmodule MyCli do
  def main(["double", arg]) do
    value = String.to_integer(arg)

    result = :rpc.call(:"my_server@127.0.0.1", MyServer, :double, [value])

    IO.puts("#{value} x2 = #{result}")
  end
end

リモート呼び出しにここでは Erlang のモジュール rpc の関数 call/4 を利用します。

まずサーバを起動します。

サーバのディレクトリでサーバを起動します。

$ _build/dev/rel/my_server/bin/my_server daemon

escript のディレクトリで iex を起動します。 このとき cookie はサーバで設定した内容に合わせます。

$ iex --name my_cli@127.0.0.1 --cookie MY_SERVER_COOKIE -S mix

関数を呼び出します。

iex(my_cli@127.0.0.1)1> MyCli.main(["double", "123"])
123 x2 = 246
:ok

無事サーバの関数を呼び出せることが確認できました。

動的にノード名と cookie を設定する

上の例では iex のオプションとしてノード名と cookie を指定していますが、escript として単独の実行ファイルとして作成したいので別の方法で設定する必要があります。

ノード名の設定は Node.start/3 を、cookie の設定は Node.set_cookie/2 を使うことで実現できます。

先の main/1 を編集して動的に設定するようにしてみます。

defmodule MyCli do
  def main(["double", arg]) do
    value = String.to_integer(arg)
    {:ok, _pid} = Node.start(:"my_cli@127.0.0.1")
    Node.set_cookie(:MY_SERVER_COOKIE)

    result = :rpc.call(:"my_server@127.0.0.1", MyServer, :double, [value])

    IO.puts("#{value} x2 = #{result}")
  end
end

start/3 に渡すノード名と set_cookie/2 に渡す cookie の値はどちらも atom である必要があります。

動作を確認します。 今回はノード名と cookie を指定せずに iex を起動します。

$ iex -S mix

main/1 を呼び出します。

iex(1)> MyCli.main(["double", "123"])
123 x2 = 246
:ok
iex(my_cli@127.0.0.1)2> 

今回も無事サーバの関数を呼び出せることが確認できました。 また main/1 が終了してもノード名が設定されたままになっていることがプロンプトからわかります。

これを終了するには関数 Node.stop/0 を呼び出します。

escript を生成する

mix.exs を編集して escript の設定を追加します。

defmodule MyCli.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_cli,
      version: "0.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      escript: [main_module: MyCli]
    ]
  end

  # 以下略

end

escript をビルドします。

$ mix escript.build

実行します。 サーバを起動しておくことを忘れないでください。

$ ./my_cli double 123
123 x2 = 246

これでコマンドラインからサーバの機能を利用することができるようになりました。

いつか読むはずっと読まない:sense of wonder

原著が 1981 年、邦訳は 1982 年。 数年前にあった復刊フェアで手に入れました。

数学パズルものや論理パズルものの一つのつもりで手にしましたが、むしろ SF 仕立ての部分が刺さります。 古典的な SF が好きな人はどうぞ。

SFパズル

SFパズル

Elixir 1.10 のアサーションの改善が、派手さはないけれどありがたい

このあいだ 1.9 がリリースされたと思っていたのに、もう 1.10 がリリースされました。

1.9 では、Elixir 単体でリリースを構築できるようになったり、config を一新したりと大きな変化がありましたが、それとくらべると今回あまり派手さを感じません。

それは Elixir が成熟期にさしかかっているということなのかもしれません。

そんな中でありがたい改善がありました。

リリースノートにも「小さいながら重要な改善」と書かれている ExUnit の改善です。

Other enhancements

ExUnit, our test framework, ships two small but important improvements: ExUnit.CaptureIO can now be used by tests that run concurrently and we have added “pattern-matching diffing”. To understand the last feature, take this code:

elixir assert %{"status" => 200, "body" => %{"key" => "foo"}} = json_payload

Now imagine that json_payload is a large JSON blob and the "key" inside the "body" did not have value of "foo".

一見それほど重要な改善には感じないのですが、データベースの操作をテストするときにとても役に立ちました。

データベースの操作のばあい、たとえば insert_atupdted_at の値は直接はコントロールできません。 このため完全一致を評価する Kernel.==/2 を使うことはできず Kernel.SpecialForms.=/2 のパタンマッチングを利用することになります。

      {:ok, user} = Accounts.create_user(%{username: "fizz buzz"})

      assert %User{username: "Fizz Buzz"} = Accounts.get_user!(user.id)

1.9 まではマッチングに失敗すると、下記のようにマッチしなかった右辺の値が出力されるだけでした。

     match (=) failed
     code:  assert %User{username: "Fizz Buss"} = Accounts.get_user!(user.id())
     right: %MyApp.Accounts.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 104, inserted_at: ~N[2020-01-29 11:47:37], updated_at: ~N[2020-01-29 11:47:37], username: "fizz buzz"}
     stacktrace:
       test/my_app/accounts_test.exs:30: (test)

これが 1.10 の改善によって左右のマッチしなかった要素が出力されるようになりました。

     match (=) failed
     code:  assert %User{username: "Fizz Buzz"} = Accounts.get_user!(user.id)
     left:  %MyApp.Accounts.User{username: "Foo Bar"}
     right: %MyApp.Accounts.User{username: "fizz buzz", __meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 85, inserted_at: ~N[2020-01-29 11:19:39], updated_at: ~N[2020-01-29 11:19:39]}
     stacktrace:
       test/my_app/accounts_test.exs:30: (test)

しかもその部分がハイライトされます。

f:id:E_Mattsan:20200129211006p:plain

これまではパタンマッチングでの評価は他の方法が使えないときの苦肉の策のような雰囲気を感じていたのですが、これで心置きなく(?)利用していくことができそうです。

C++ による NIF ことはじめ

使い慣れている言語が C++ なので、ここでは C++ で説明しますが基本的に C 言語でも同じように記述できるはずです。 というか、世の中の NIF の説明は圧倒的に C 言語が多い。

詳細の解説は置いておいて。コードを書いてから呼び出せるところまでを順にたどってゆきます。

(不備など発見されましたらご指摘いただけると幸いです)

C++ のコードを用意する

二つの整数の和を返す add(int, int) を定義し、これを Elixir から呼び出すインタフェースを書きます。

#include <erl_nif.h>

int add(int x1, int x2) {
  return x1 + x2;
}

ERL_NIF_TERM add_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  int x1;
  int x2;

  if (!enif_get_int(env, argv[0], &x1)) {
    return enif_make_badarg(env);
  }

  if (!enif_get_int(env, argv[1], &x2)) {
    return enif_make_badarg(env);
  }

  int result = add(x1, x2);

  return enif_make_int(env, result);
}

ErlNifFunc nif_funcs[] = {
  {"add", 2, add_nif}
};

ERL_NIF_INIT(Elixir.Calc, nif_funcs, nullptr, nullptr, nullptr, nullptr);

add_nif が Elixir とインタフェースする関数です。 Elixir から呼び出される関数の型はこの形で定義します。

enif_get_int で引数を整数型として値を取得します。

戻り値は、演算結果を enif_make_int で Elixir の整数としての値を構築して返します。 また引数エラーの場合は enif_make_badarg で例外を構築して返します。

ERL_NIF_INIT で初期化を定義します。

# 説明
1 モジュール名を指定します。Elixir のモジュール名は VM 上では Elixir. という接頭辞つきで扱われます。ここではその接頭辞つきの名前で指定します。
2 関数の定義の配列を指定します。個々の定義は、Elixir での関数名, 関数のアリティ, C++ の関数のポインタ の配列からなります。
3 ライブラリがロードされたときに呼び出される関数を指定します。
4 OTP 20 以降利用されなくなりました。
5 モジュールのアップグレイドが発生したときに呼び出される関数を指定します。
6 ライブラリがアンロードされたときに呼び出される関数を指定します。

詳しくはドキュメントを参照してください。

C++ のコードをコンパイルする

コードをコンパイルする前に。erl_nif.h のパスを確認します。

ヘッダファイルは :code.root_dir/0 で取得できるパスの下、 usr/include に配置されています。

$ ls $(elixir -e 'IO.puts(:code.root_dir())')/usr/include
driver_int.h
ei.h
ei_connect.h
eicode.h
erl_driver.h
erl_drv_nif.h
erl_fixed_size_int_types.h
erl_int_sizes_config.h
erl_interface.h
erl_memory_trace_parser.h
erl_nif.h
erl_nif_api_funcs.h

コンパイルしたライブラリの配置について、はっきりとしたルールを見つけることができなかったのですが、種々のリソースを配置している priv に置くことにしました。

$ mkdir priv
$ g++ -I$(elixir -e 'IO.puts(:code.root_dir())')/usr/include \
      -fPIC \
      -shared \
      -undefined dynamic_lookup \
      -o priv/calc.so \
      c_src/calc.cpp

C++ で作成したライブラリを Elixir で読み込む

作成したライブラリを利用する Elixir 側のコードです。

ライブラリのロードは :erlang.load_nif/2 を利用します。 第 1 引数にはライブラリの拡張子を除いたパスを指定します。 第 2 引数にはライブラリに渡す値を指定します。 この値は ERL_NIF_INIT の第 3 引数に指定した関数に渡されます。

ライブラリを配置したディレクトリのパスは :code.priv_dir/1 で取得します。 この関数は _build ディレクトリの下にある priv の場所を返してくれます。 _build ディレクトリの下にある priv は通常はプロジェクト直下にある priv へのシンボリックリンクですが、リリース時には実際にディレクトリが作成され priv の内容がコピーされます。

開発時とリリース時で同じように参照できるので :code.priv_dir/1 を利用してライブラリをロードするようにしています。

またモジュールがロードされたときに自動的にライブラリをロードするように @on_load でモジュールのロードをフックし、そこでライブラリをロードしています。

defmodule Calc do
  @on_load :load_nifs

  def load_nifs do
    :code.priv_dir(:calc)
    |> Path.join("calc")
    |> :erlang.load_nif(0)
  end

  def add(_, _) do
    raise "NIF has not been loaded"
  end
end

ここまで準備ができればあとは Calc モジュールを Elixir で書いたモジュールと同じように利用することができます。

$ iex -S mix
iex(1)> Calc.add(111, 222)
333
iex(2)> Calc.add(1.1, 2.2) 
** (ArgumentError) argument error
    (calc) Calc.add(1.1, 2.2)

C++コンパイルを mix compile にまかせる

ここまで C++ のコードは Elixir のコードとは別にコンパイルしましたが、mix compile コマンドなどで Elixir のコードがコンパイルされるときに一緒にコンパイルされると便利です。 C++コンパイルを Elixir のコードで記述し、コンパイル時に実行するコードに含めることでそれを実現できます。

まず Mix.Tasks.Compile.Calc というモジュールを mix.exs の中で定義し、run/1 という関数を記述します。 その中で System.cmd/3 を利用して C++コンパイルを実行します。

次に project/0 で定義しているキーワードリストに compilers という項目を追加します。この値はデフォルトでは Mix.compilers/0 が返すリストの値になっていますが、今回記述したモジュールをこのリストに追加します。

defmodule Calc.MixProject do
  use Mix.Project

  def project do
    [
      # ...
      compilers: [:calc | Mix.compilers()]
    ]
  end

  # ...
end

defmodule Mix.Tasks.Compile.Calc do
  def run(_argv) do
    erl_path = Path.join(:code.root_dir(), "usr")
    include_path = Path.join(erl_path, "include")
    target_path = Path.join("priv", "calc.so")

    File.mkdir_p("priv")

    {result, _} =
      System.cmd(
        "g++",
        [
          "-I#{include_path}",
          "-fPIC",
          "-undefined", "dynamic_lookup",
          "-shared",
          "-o", target_path,
          "c_src/calc.cpp"
        ],
        stderr_to_stdout: true
      )

    IO.puts(result)
  end
end

これでライブラリが作成されていない状態からでも mix compileiex -S mix といった Elixir のコードがコンパイルされるのと合わせてライブラリの作成の実行されるようになりました。

コンパイルの手間を減らすパッケージを利用する

C++ のコードのコンパイルMakefile で管理することが多いと思いますが、Makefile を利用したコンパイルを手助けしてくれるパッケージが提供されています。

いつか読むはずっと読まない:数論は数学の女王

数の女王

数の女王

  • 作者:川添 愛
  • 出版社/メーカー: 東京書籍
  • 発売日: 2019/07/16
  • メディア: 単行本(ソフトカバー)

「黒のマティルデ」の数についてはこちらに詳しく書かれています。

Phoenixの認証をGuardianに護らせる

Phoenix で作成したアプリケーションに、権利のあるユーザにのみアクセスを許可したいばあいに Guardian という Elixir のパッケージを利用して実現する方法の覚書です。

基盤にしている JWT などの技術に今のところ明るくないこともあり、ここでは手順だけ記述します。

ドキュメントにも手順が書かれているのですが、記述が別れていたり微妙にわかりにくい部分があっりしたので自分で試したものを書き下してみました。

Phoenix app を用意する

認証機能を実装したい Phoenix app を用意します。

ここでは my_app という名で作成し、以降もこの名前で説明をしてゆきます。

$ mix phx.new my_app

アカウントを管理する仕組みを用意する

ユーザがアプリケーションにログインすることを想定して、認証されるアカウントを管理する仕組みを用意します。

実際にはユーザ情報をデータベースに格納するところですが、この記事では簡単に特定のユーザ名とパスワードの組を受け付ける仕組みで代用します。

lib/my_app/accounts/user.ex

ユーザデータを格納する構造体です。

defmodule MyApp.Accounts.User do
  defstruct [:username, :password]
end

lib/my_app/accounts.ex

ユーザ名とパスワードの組で認証するモジュールです。 実装はユーザ名、パスワードとも固定にしています。

defmodule MyApp.Accounts do
  alias MyApp.Accounts.User

  @username "foobar"
  @password "FooBar"

  @doc """
  usename をキーにユーザを取得する

  ユーザが存在する場合は、ユーザデータを返します。

  ユーザが存在しない場合は、 `nil` を返します。

  ## Example

      iex> MyApp.Accounts.get_user_by_username("foobar")
      %MyApp.Accounts.User{username: "foobar", password: "FooBar"}

      iex> MyApp.Accounts.get_user_by_username("hoge")
      nil
  """
  def get_user_by_username(username) do
    case username do
      @username -> %User{username: @username, password: @password}
      _ -> nil
    end
  end

  @doc """
  ユーザ名とパスワードで認証する

  認証に成功した場合は、 `:ok` とユーザデータのタプルを返します。

  認証に失敗した場合は、 `:error` と失敗理由のタプルを返します。

  ## Example

      iex> MyApp.Accounts.authenticate_user("foobar", "FooBar")
      {:ok, %MyApp.Accounts.User{username: "foobar", password: "FooBar"}}

      iex> MyApp.Accounts.authenticate_user("hoge", "Hoge")
      {:error, :unauthorized}
  """
  def authenticate_user(username, password) do
    case {username, password} do
      {@username, @password} -> {:ok, %User{username: @username, password: @password}}
      _ -> {:error, :unauthorized}
    end
  end
end

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

mix.exs ファイルを編集して deps/0 に記述される依存パッケージに Guardian を追加します。

mix hex.info コマンドで確認すると、この記事を書いた時点の最新バージョンは 2.0 でしたので、mix.exs でもこのバージョンを指定することにします。

$ mix hex.info guardian                                                                                                                                                      
Elixir Authentication framework                                                                                                                                                     
                                                                                                                                                                                    
Config: {:guardian, "~> 2.0"}                                                                                                                                                       
Releases: 2.0.0, 1.2.1, 1.2.0 (retired), 1.1.1, 1.1.0, 1.0.1, 1.0.0, 1.0.0-beta.1, ...                                                                                              
                                                                                                                                                                                    
Licenses: MIT                                                                                                                                                                       
Links:                                                                                                                                                                              
  Github: https://github.com/ueberauth/guardian  

mix.exs を編集して {:guardian, "~> 2.0"} の記述を追加します。

--- a/mix.exs
+++ b/mix.exs
@@ -38,7 +38,8 @@ defmodule MyApp.MixProject do
       {:phoenix_live_reload, "~> 1.2", only: :dev},
       {:gettext, "~> 0.11"},
       {:jason, "~> 1.0"},
-      {:plug_cowboy, "~> 2.0"}
+      {:plug_cowboy, "~> 2.0"},
+      {:guardian, "~> 2.0"}
     ]
   end
 end

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

 $ mix deps.get

Guardian モジュールを記述する

Guardian パッケージを利用したモジュールを 3 つ定義します。

パッケージのドキュメントにあるサンプルでは lib/my_app/ にモジュールを定義しているのですが、認証は Web サービスの領分と思うのでここでは lib/my_app_web/ に定義することにしました(この点、ご意見いただけるとさいわいです)。

lib/my_app_web/auth/
├── error_handler.ex
├── guardian.ex
└── pipeline.ex

lib/my_app_web/auth/guardian.ex

Guardian の実装です。

ここで resource はユーザデータに当たり、token はユーザ名 (username) に当たります。

defmodule MyAppWeb.Auth.Guardian do
  use Guardian, otp_app: :my_app

  alias MyApp.Accounts

  def subject_for_token(resouce, _claims) do
    {:ok, resouce.username}
  end

  def resource_from_claims(%{"sub" => username}) do
    case Accounts.get_user_by_username(username) do
      nil -> {:error, :resource_not_found}
      user -> {:ok, user}
    end
  end
end

lib/my_app_web/auth/error_handler.ex

認証に失敗したときに実行されるエラーハンドラです。 Guardian.Plug.ErrorHander を実装します。

defmodule MyAppWeb.Auth.ErrorHandler do
  @behaviour Guardian.Plug.ErrorHandler

  import Plug.Conn

  def auth_error(conn, {type, _reason}, _opts) do
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(401, to_string(type))
  end
end

lib/my_app_web/auth/pipeline.ex

上で実装したモジュールを使ってパイプラインを定義します。

defmodule MyAppWeb.Auth.Pipeline do
  use Guardian.Plug.Pipeline,
    otp_app: :my_app,
    error_handler: MyAppWeb.Auth.ErrorHandler,
    module: MyAppWeb.Auth.Guardian

  plug Guardian.Plug.VerifySession
  plug Guardian.Plug.VerifyHeader
  plug Guardian.Plug.LoadResource, allow_blank: true
end

router にパイプラインを追加する

lib/my_app_web/router.ex を編集してパイプラインを追加します。

  pipeline :auth do
    plug MyAppWeb.Auth.Pipeline
  end

  pipeline :ensure_auth do
    plug Guardian.Plug.EnsureAuthenticated
  end

セッションを管理する仕組みを用意する

ユーザ名とパスワードを入力するフォームと、入力された情報をハンドリングするコントローラを実装します。

lib/my_app_web/templates/session/new.html.eex

<%= form_for :user, Routes.session_path(@conn, :create), fn f -> %>

  <%= label f, :username %>
  <%= text_input f, :username %>

  <%= label f, :password %>
  <%= password_input f, :password %>

  <%= submit "login" %>
<% end %>

lib/my_app_web/views/session_view.ex

defmodule MyAppWeb.SessionView do
  use MyAppWeb, :view
end

lib/my_app_web/controllers/session_controller.ex

コントローラでは、入力されたユーザ名とパスワードを MyApp.Accounts.authenticate_user/2 で認証にかけます。

成功したばあい、Guardian.Plug.sign_in/5 でユーザのトークンをセッションに記憶します。 ユーザ情報からのトークンの抽出は、先に定義したモジュール MyAppWeb.Auth.Guardian の関数が利用されます。

トークンはログアウトの際の Guardian.Plug.sign_out/3 でセッションから削除されます。

defmodule MyAppWeb.SessionController do
  use MyAppWeb, :controller

  def new(conn, _) do
    conn
    |> render("new.html")
  end

  def create(conn, %{"user" => %{"username" => username, "password" => password}}) do
    case MyApp.Accounts.authenticate_user(username, password) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "Welcome back!")
        |> Guardian.Plug.sign_in(MyAppWeb.Auth.Guardian, user)
        |> redirect(to: Routes.page_path(conn, :index))

      {:error, reason} ->
        conn
        |> put_flash(:error, to_string(reason))
        |> new(%{})
    end
  end

  def destroy(conn, _) do
    conn
    |> Guardian.Plug.sign_out(MyAppWeb.Auth.Guardian, [])
    |> redirect(to: Routes.session_path(conn, :new))
  end
end

routing を設定する

ルーティングに先ほど定義したパイプラインを設定していきます。

アプリケーションを作成した時点ではルーティングはこのようになっています。

   scope "/", MyAppWeb do
    pipe_through :browser
 
     get "/", PageController, :index
   end

これを次のように変更します。

  scope "/", MyAppWeb do
    pipe_through [:browser, :auth]

    get "/login", SessionController, :new
    post "/session", SessionController, :create
  end

  scope "/", MyAppWeb do
    pipe_through [:browser, :auth, :ensure_auth]

    get "/", PageController, :index
    delete "/session", SessionController, :destroy
  end

これでパイプラインの :ensure_auth を利用するスコープは認証がすんでいないとアクセスすることができなくなりました。

config を設定する

Guardian の設定として、アプリケーションと秘密鍵を config で設定します。 ここでは秘密鍵環境変数から取得するようにしています。

config :my_app, MyAppWeb.Auth.Guardian,
  issuer: "my_app",
  secret_key: System.get_env("GUARDIAN_SECRET_KEY")

mix guardian.gen.secret を利用して秘密鍵を生成し、環境変数に設定します。

$ export GUARDIAN_SECRET_KEY=`mix guardian.gen.secret`

護られていることを確認する

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

$ mix phx.server

この状態で http://localhost:4000 にアクセスすると、認証がすんでいないので unauthenticated と表示されます。

次にログインしてみます。

http://localhost:4000/login にアクセスします。 フォームが表示されるので用意しておいたユーザ名とパスワード foobarFooBar を入力します。 入力が正しければ先ほど拒否されていた http://localhost:4000 にリダイレクトされ Welcome back! の文字が表示されるはずです。

ログアウトする

ログアウト機能を追加します。

lib/my_app_web/templates/layout/app.html.eex

lib/my_app_web/templates/layout/app.html.eex を開き、Get started と記述されている行の下にログアウトのリンクを作成します。

Guardinan.Plug.curent_resource/2 関数でログイン時のリソースが取得できます。 具体的には今回はユーザのレコードをリソースとしているのでこの関数でユーザのレコードを得ることができます。

これを利用してログインしているばあいだけログアウトのリンクを表示するようにします。

            <%= if Guardian.Plug.current_resource(@conn) do %>
              <li><%= link "logout", to: Routes.session_path(@conn, :destroy), method: :delete %></li>
            <% end %>

ログインを促す

認証前に認証が必要なページにアクセスしたばあい今は unauthenticated と表示するだけですが、これをログインページにリダイレクトするようにします。

Guardian.Plug.ErrorHandler.auth_error/3 を次のように変更します。 フラッシュにメッセージを設定する関数 put_flash/3 とリダイレクトの関数 redirect/2 の二つを利用していますが、扱いやすくするため定義されているモジュール Phoenix.Controller を import して利用しています。

defmodule MyAppWeb.Auth.ErrorHandler do
  @behaviour Guardian.Plug.ErrorHandler

  import Plug.Conn

  import Phoenix.Controller, only: [put_flash: 3, redirect: 2]
  alias MyAppWeb.Router.Helpers, as: Routes

  def auth_error(conn, {type, _reason}, _opts) do
    conn
    |> put_flash(:error, "unauthorized")
    |> redirect(to: Routes.session_path(conn, :new))
  end
end

これでログインしてない状態で http://localhost:4000 にアクセスしたとき、ログインページにリダイレクトされるようになりました。

いつか読むはずっと読まない:習慣の集まり

メリットの法則 行動分析学・実践編 (集英社新書)

メリットの法則 行動分析学・実践編 (集英社新書)

黒魔術で Elixir に Ruby を召喚する

まず始めに。

ネタ記事です。

ひさびさのネタ記事です。

ネタプログラミングです。

とはいえ。

黒魔術を使うことはなくても、このようなしくみを知っていると、その知識が役に立つときが来るかもしれません(来ないかもしれません)。

Level 1, Medium: Port.open/2 で Ruby を起動する

まずは定石の範囲で。

Port.open/2 を使って Ruby のプロセスを起動します。

外部プロセスの起動には System.cmd/3 という関数もありますが、 Port.open/2 の方が制御を細かくできるのでこちらを利用しすることにします。 ちなみに System.cmd/3 も内部では Port.open/2 を利用していました。

path = System.find_executable("ruby")
port = Port.open({:spawn_executable, path},
         [:binary, args: ["-e", "puts('Hello')"]])

receive do
  {^port, {:data, data}} -> data
end
# => "Hello\n"

Port.open/2 でプロセスを起動すると、そのプロセスの出力をメッセージで送信します。 メッセージはポートの値とデータのタプル、データは :data とデータ本体の値のタプル、という形式になっています。

メッセージの形式の詳細はドキュメントを参照してください。

Port.open/2 の戻り値を送り先に指定すると、起動したプロセスにメッセージを送ることができます。

path = System.find_executable("ruby")

# 1 行読み込んで 1 行出力する
port = Port.open({:spawn_executable, path}, [:binary, args: ["-e", ~S|puts("Hello #{gets.strip}!")|]])

# 1 行分の文字列を送る
send(port, {self(), {:command, "world\n"}})

receive do
  {^port, {:data, data}} -> data
end
# => "Hello world!\n"

Level 2, Seer: irb を起動して対話的に式を評価する

ruby コマンドでは、ループ処理を自分で描かない限り、ワンショットの実行になってしまいます。 そこで代わりに irb を起動して一つのプロセスで何度でも式を評価できるようにしてみます。

path = System.find_executable("irb")
port = Port.open({:spawn_executable, path}, [:binary])

ここで。irb を起動しするとどのような挙動になるか IEx で flush/0 を使って確認してみます。

iex> path = System.find_executable("irb")
iex> port = Port.open({:spawn_executable, path}, [:binary])
#Port<0.5>

iex> flush
{#Port<0.5>, {:data, "Switch to inspect mode.\n"}}

起動しただけでなにやらメッセージを受信しています。

メッセージとして Ruby の式を送ってみます。 評価は行単位で行われるので末尾の改行( "\n" )を忘れないようにしてください。

# 式展開で書くと Elixir の文脈で展開してしまうので文字列連結 ++ で書いています
iex> send(port, {self(), {:command, "puts('Hello ' ++ (gets.strip) ++ '!')\n"}})

# gets が読み込む文字列を送る
iex> send(port, {self(), {:command, "world\n"}})
{#PID<0.108.0>, {:command, "world\n"}}

iex> flush
{#Port<0.5>, {:data, "puts('Hello ' ++ (gets.strip) ++ '!')\n"}}
{#Port<0.5>, {:data, "Hello world!\n"}}
{#Port<0.5>, {:data, "nil\n"}}

入力した評価対象の文字列( "puts('Hello ' ++ (gets.strip) ++ '!')\n" )と、puts の出力結果、そして puts の戻り値の値である nil が送り返されました。

出力した内容( "Hello irb!\n" )だけが得られればよいのですが、入力した評価対象の内容も送られてきてしまっています。

明示的に出力した内容以外の出力を抑制するオプションをつけて irb を起動するようにします。

iex> path = System.find_executable("irb")
# 評価する式の出力を抑える --noverbose と 評価した値の出力を抑える --noecho を指定する
iex> port = Port.open({:spawn_executable, path}, [:binary, args: ["--noverbose", "--noecho"]])

iex> send(port, {self(), {:command, "puts('Hello ' ++ (gets.strip) ++ '!')\n"}})
iex> send(port, {self(), {:command, "world\n"}})

iex> flush
{#Port<0.5>, {:data, "Hello world!\n"}}

これで評価する式と評価した値、加えて起動時のメッセージの出力を抑制することができました。

Level 3, Conjuror: サーバにする

これらを踏まえて。 Ruby の式を評価して結果を返すサーバを書いてみます。

defmodule ExIRB do
  use GenServer

  @name __MODULE__

  def start_link(opts) do
    name = Keyword.get(opts, :name, @name)
    GenServer.start_link(__MODULE__, opts, name: name)
  end

  def eva_ruby(server \\ @name, expression) do
    GenServer.call(server, {:eva_ruby, expression})
  end

  def init(_opts) do
    path = System.find_executable("irb")
    port = Port.open({:spawn_executable, path}, [:binary, args: ["--noverbose", "--noecho"]])

    {:ok, %{port: port, froms: []}}
  end

  def handle_call({:eva_ruby, expression}, from, %{port: port, froms: froms} = state) do
    send(port, {self(), {:command, expression}})

    {:noreply, %{state | froms: [from | froms]}}
  end

  def handle_info({port, {:data, data}}, %{port: port, froms: [from | froms]} = state) do
    GenServer.reply(from, data)

    {:noreply, %{state | froms: froms}}
  end
end

実行。

iex> ExIRB.start_link([])
iex> ExIRB.eva_ruby("puts('Hello ExIRB!')\n")
"Hello ExIRB!\n"

ここで注意点として。この実装では式を評価したら必ずメッセージが返されることを期待しています。 値を出力しない式を与えるとタイムアウトが発生します。

値が返ることを期待しない式を評価したい場合は、次のような関数とハンドラを追加することで実現できます。

  def cast_ruby(server \\ @name, expression) do
    GenServer.cast(server, {:cast_ruby, expression})
  end
  def handle_cast({:cast_ruby, expression}, %{port: port} = state) do
    send(port, {self(), {:command, expression}})

    {:noreply, state}
  end
# irb は gets の入力待ちになる
iex> ExIRB.cast_ruby("puts('Hello ' ++ (gets.strip) ++ '!')\n")

# 文字列を送ると gets が値を返し、結果として上の式が評価された値が返される
iex> ExIRB.eva_ruby("world\n")
"Hello world!\n"

ここで puts を使っていると末尾に改行コードが付いてしまいます。 これは print に変更することでこれを回避できます。

iex> ExIRB.eva_ruby("print('Hello ExIRB!')\n")                
"Hello ExIRB!"

Level 4, Magician: マーシャリングした値を利用する

irb のプロセスを起動し評価したい Ruby の式を渡すことで値を得ることができるようになりました。 しかし文字列という形式でしかやりとりができません。 その文字列を Elixir 上でパースすることもできますが面倒です。

シリアライズした Ruby の値を Elixir でデシリアライズできれば文字列をパースするよりもミスなく値を受け渡すことができるはずです。

と、いうわけで。

Ruby の値を Marshal で出力し、それを Elixir で読み込むことにします。

Format of marshaling

フォーマットの仕様はドキュメントにまとめられています。

フォーマットのバージョンは Ruby 1.8.0 以降は 4.8 なので、先頭の 2 バイトは 0x040x08 です。

niltruefalse はそれぞれ "0""T", "F" という形式になるので、マーシャリングされると

Ruby の値 マーシャリングされた値
nil "\x04\x080"
true "\x04\x08T"
false "\x04\x08F"

irb で確認してみます。

irb> Marshal.load("\x04\x080")
=> nil
irb> Marshal.load("\x04\x08T")
=> true
irb> Marshal.load("\x04\x08F")
=> false

Level 3 で作成した ExIRB で同じように評価してみます。

iex> ExIRB.eva_ruby("print(Marshal.dump(nil))\n")
<<4, 8, 48>>
iex> ExIRB.eva_ruby("print(Marshal.dump(true))\n")
<<4, 8, 84>>
iex> ExIRB.eva_ruby("print(Marshal.dump(false))\n")
<<4, 8, 70>>

文字として表示できない値が含まれるためバイト列として表示されます。 フォーマットが規定されているバイト列のパースは文字列のパースよりも容易ですし、なにより Elixir はバイト列のパースが得意です。

そんなわけで。

マーシャリングされた Ruby の値を Elixir の値に変換するパッケージを がっ! と書いてみました。

mix.exsdepsex_marshal を追加します。

# mix.exs

  defp deps do
    [
      {:ex_marshal, github: "mattsan/ex_marshal"}
    ]
  end

iex を起動しなおし、マーシャリングした値を ExMarshal.load/1 で読み込んでみます。

iex> ExIRB.start_link([])
iex> ExIRB.eva_ruby("print(Marshal.dump(nil))\n") |> ExMarshal.load()
nil
iex> ExIRB.eva_ruby("print(Marshal.dump(false))\n") |> ExMarshal.load()
false
iex> ExIRB.eva_ruby("print(Marshal.dump(true))\n") |> ExMarshal.load()
true
iex> ExIRB.eva_ruby("print(Marshal.dump(123))\n") |> ExMarshal.load()
123
iex> ExIRB.eva_ruby("print(Marshal.dump([1,2,3]))\n") |> ExMarshal.load()
[1, 2, 3]
iex> ExIRB.eva_ruby("print(Marshal.dump({a: 1, b: '2', c: :three}))\n") |> ExMarshal.load()
%{a: 1, b: "2", c: :three}

扱える値はドキュメント( lib/ex_marshal.ex の moduledoc )を参照してください。

Ruby のコードを外部の関数として扱うと割り切ればマーシャリングの処理をハンドラの中に隠すことができます。 思い切って書き換えてみます。

  def handle_call({:eva_ruby, expression}, from, %{port: port, froms: froms} = state) do
    send(port, {self(), {:command, "print(Marshal.dump(#{expression}))\n"}})

    {:noreply, %{state | froms: [from | froms]}}
  end

  def handle_info({port, {:data, data}}, %{port: port, froms: [from | froms]} = state) do
    GenServer.reply(from, ExMarshal.load(data))

    {:noreply, %{state | froms: froms}}
  end

修正したコードの内容を反映して Ruby の式を評価してみます。

iex> ExIRB.eva_ruby("nil")
nil
iex> ExIRB.eva_ruby("true")
true
iex> ExIRB.eva_ruby("true")
true
iex> ExIRB.eva_ruby("false")
false
iex> ExIRB.eva_ruby("123")
123
iex> ExIRB.eva_ruby("[1,2,3]")
[1, 2, 3]
iex> ExIRB.eva_ruby("{a: 1, b: '2', c: :three}")
%{a: 1, b: "2", c: :three}

文字列の中は Ruby の式として評価されるのでメソッドも記述できます。 結果は ExMarshal.load/1 で変換しているので Elixir の値として返されます。

iex> ExIRB.eva_ruby("(1..10).map {|i| i * i }")
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Level 5, Enchanter: ライブラリを読み込む

irbコマンドラインオプション -r で起動時にライブラリを読み込むことができます。

ここでは ExIRB.start_link/1 の引数で指定できるようにしてみます。

ExIRB.init/1 を次のように書き換えます。

  def init(opts) do
    path = System.find_executable("irb")
    requires =
      opts
      |> Keyword.get(:require, [])
      |> Enum.map(&"-r#{&1}")
    port = Port.open({:spawn_executable, path}, [:binary, args: ["--noverbose", "--noecho" | requires]])

    {:ok, %{port: port, froms: []}}
  end

オプションで :require で指定された値をライブラリ名のリストとして -r をつけて irb のオプションの形式にします。 Port.open/2 の起動時の引数にそれらを渡します。

ExIRB.start_link/1 でライブラリを指定します。

iex> ExIRB.start_link(require: ["yaml"])

ここでは yaml を指定しています。 これで YAML のメソッドを利用することができるようになりました。

iex> ExIRB.eva_ruby("YAML.load('---\n- 1\n- 2\n')")
[1, 2]

混乱しそうですが、こんなこともできます。

iex> yaml = """
...> ---
...> - 1
...> - 2
...> - a:
...>   - 3
...>   - 4
...> """
"---\n- 1\n- 2\n- a:\n  - 3\n  - 4\n"
iex> ExIRB.eva_ruby("YAML.load('#{yaml}')")
[1, 2, %{"a" => [3, 4]}]

Level 6, Warlock: apply(module, function, arguments)

Ruby の式を文字列で記述する代わりにモジュール名とメソッド名と引数列を評価できれば Elixir の値を渡すことができます。 ちょうど Kernel.apply/3 のような記述です。

このためにはまず引数列を文字列に変換する必要がありますが、これは個々の値を Kernel.inspect/2 で評価してコンマで連結すれば良さそうです。

iex> args = [1, "2", :three] |> Enum.map(&inspect/1) |> Enum.join(",")
"1,\"2\",:three"

iex> "Foo.bar(#{args})"
"Foo.bar(1,\"2\",:three)"

よさそうなのですが、いくつか落とし穴があるので注意しなければなりません。

長い値は省略される

要素の数が多く表現が長くなるばあい、途中から省略されてしまいます。

iex> (1..55) |> Enum.to_list() |> inspect()
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, ...]"

すべての要素を表示させるためには :limit オプションに :infinity を指定します。

iex> (1..55) |> Enum.to_list() |> inspect(limit: :infinity)
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55]"

表示できる文字コードのリストは charlist として表示される

Erlang や Elixir の「クセ」の部分。 整数値のリストなのに、その整数値が文字コードとして表示できるばあい、文字のリスト (charlist) として表現されてしまいます。

iex> (65..90) |> Enum.to_list() |> inspect(limit: :infinity)
"'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"

整数値のリストとして表示させるためには :charlists オプションに false を指定します。

iex> (65..90) |> Enum.to_list() |> inspect(limit: :infinity, charlists: false)
"[65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90]"

マップはには % が付く

マップを inspect すると、%{} で囲まれた文字列になります。 これは Elixir の仕様ですから、もうどうしようもありません。

iex> %{a: 1} |> inspect(limit: :infinity, charlits: false)
"%{a: 1}"

…と、あきらめかけていたのですが。 バージョン 1.9.0 から :inspect_fun オプションに適切な変換を行う関数を渡すことで任意の表示をさせることができるようになりました。

たとえば次のような関数 MapToHash.inspect_fun/2 を用意します。

defmodule MapToHash do
  import Inspect.Algebra

  def inspect_fun(term, opts) when is_map(term) do
    arrow = string("=>")
    comma = string(",")

    docs =
      term
      |> Enum.reduce([string("}")], fn {k, v}, acc ->
        key = inspect_fun(k, opts)
        value = inspect_fun(v, opts)
        [comma, key, arrow, value | acc]
      end)

    docs =
      case docs do
        [^comma | docs] -> docs
        docs -> docs
      end

    concat([string("{") | docs])
  end

  def inspect_fun(term, opts) do
    Inspect.inspect(term, opts)
  end
end

この関数を :inspect_fun オプションに指定します。

iex> inspect(%{a: 1}, limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
"{:a=>1}"

入れ子になったマップでも空のマップでも Ruby のハッシュの形式の文字列で出力できます。

iex> inspect(%{a: 1, b: %{c: %{d: :e}}}, limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
"{:b=>{:c=>{:d=>:e}},:a=>1}"
iex> inspect(%{}, limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
"{}"

Elixir のモジュール名には Elixir が付いている

あとはモジュール名と関数名とを連結して先に書いた関数で Ruby にメッセージを送ればよさそうです。

まず期待する文字列を組み立てられるか、次のような関数を書いて確かめてみます。

  def apply(module, function, args) do
    args_str =
      args
      |> Enum.map(fn arg ->
        arg
        |> inspect(limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
      end)
      |> Enum.join(",")

    "#{module}.#{function}(#{args_str})"
  end

モジュール名と関数名を文字列で指定してみます。

iex> ExIRB.apply("YAML", "load", ["---\n- 1\n - 2\n"])
"YAML.load(\"---\\n- 1\\n - 2\\n\")"

よさそうです。

次に関数名をアトムで指定してみます。

iex> ExIRB.apply("YAML", :load, ["---\n- 1\n - 2\n"])
"YAML.load(\"---\\n- 1\\n - 2\\n\")"

これもよさそうです。

最後に kernel.apply/3 と同じようにモジュール名で指定してみます。

iex> ExIRB.apply(YAML, :load, ["---\n- 1\n - 2\n"])
"Elixir.YAML.load(\"---\\n- 1\\n - 2\\n\")"

Elixir の文字列がつきました。

Elixir のモジュール名の実態はアトムです。

iex> is_atom(YAML)
true

ですのでモジュールが定義されていなくてもモジュール名は自由に使うことができます。 ただし注意する点がああります。たとえば YAML と書いたばあい内部的には :"Elixir.YAML" という値になっているという点です。 これは IEx でも簡単に確認することができます。

iex> :"Elixir.YAML"
YAML

iex> :"Elixir.YAML" == YAML
true

モジュールは Module.split/1 を使うと安全に文字列として分割できるので今回はこれを使うことにします。

iex> [module_name] = Module.split(YAML)
["YAML"]
iex> module_name
"YAML"

上に書いた apply 関数を修正して、組み立てた文字列を Ruby で評価する関数にします。

  def apply(server \\ @name, module, function, args) do
    [module_name] = Module.split(module)
    args_str =
      args
      |> Enum.map(fn arg ->
        arg
        |> inspect(limit: :infinity, charlists: false, inspect_fun: &MapToHash.inspect_fun/2)
      end)
      |> Enum.join(",")

    GenServer.call(server, {:eval_ruby, "#{module_name}.#{function}(#{args_str})"})
  end
iex> ExIRB.start_link(require: ["yaml"])
iex> ExIRB.apply(YAML, :load, ["---\n- 1\n- 2\n"])
[1, 2]
iex> ExIRB.apply(YAML, :dump, [%{a: 1}])
"---\n:a: 1\n"

これで Kernel.apply/3 と同じ書式で Ruby のメソッドを呼び出せるようになりました。

Level 7, Sorcerer: 関数を定義する

apply できるようになりましたが、やはり本来の関数名で呼び出したいものです。

関数を定義してみましょう。

defmodule ExYAML do
  def load(str), do: ExIRB.apply(YAML, :load, [str])
  def dump(term), do: ExIRB.apply(YAML, :dump, [term])
end
iex> ExIRB.start_link(require: ["yaml"])
iex> ExYAML.dump(%{a: 1})
"---\n:a: 1\n"
iex(3)> ExYAML.load("---\n- 1\n- 2\n")
[1, 2]

呼び出しは便利になりましたが関数を定義するのが面倒です。

そんなときは。 そう、マクロです。

関数を定義するマクロを定義します。

defmodule Importer do
  defmacro import_ruby(module, function) do
    quote do
      def unquote(function)(args) do
        ExIRB.apply(unquote(module), unquote(function), args)
      end
    end
  end
end

関数を定義したいモジュールでマクロを展開します。

defmodule ExYAML do
  require Importer
  import Importer

  import_ruby YAML, :load
  import_ruby YAML, :dump
end

これだけで関数が使えるようになります。

iex> ExIRB.start_link(require: ["yaml"])
iex> ExYAML.dump([%{a: 1}])
"---\n:a: 1\n"
iex(3)> ExYAML.load(["---\n- 1\n- 2\n"])
[1, 2]

しかし。このままでは一つの引数しか受け取れないので複数の引数を渡したいときにはリストにまとめて渡す必要があります。

引数の数も定義できる方法を考えます。

まず、quote された関数の構造を調べます。

iex> quote do
...>   def dump(term, opts) do
...>     ExIRB.apply(YAML, :dump, [term, opts])
...>   end
...> end
{:def, [context: Elixir, import: Kernel],
 [
   {:dump, [context: Elixir], [{:term, [], Elixir}, {:opts, [], Elixir}]},
   [
     do: {{:., [], [{:__aliases__, [alias: false], [:ExIRB]}, :apply]}, [],
      [
        {:__aliases__, [alias: false], [:YAML]},
        :dump,
        [{:term, [], Elixir}, {:opts, [], Elixir}]
      ]}
   ]
 ]}

二つの引数 (term, opts) は、[{:term, [], Elixir}, {:opts, [], Elixir}] という構造に展開されていることがわかります。 また ExIRB.apply/3 の三つ目の引数 [term, opts] も同じ構造になっています。

つまり必要な引数の数だけこの構造を並べたものをマクロの値として返すようにすれば、欲しい引数の数の関数を得ることができるはずです。

引数の構造を組み立てます。 26 個より多い引数を持つ関数はないだろうという大雑把な考えのもと、Stream で個々の引数のタプルを組み立て Enum.take/2 で切り出します。

args =
  (?a..?z)
  |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
  |> Enum.take(arity)

これを関数の定義に組み込みます。 全体では、このようになります。

defmodule Importer do
  defmacro import_ruby(module, function, arity) do
    args =
      (?a..?z)
      |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
      |> Enum.take(arity)

    {
      :def, [context: __MODULE__, import: Kernel],
      [
        {
          function,
          [context: __MODULE__],
          args
        },
        [do: {
          {:., [], [ExIRB, :apply]},
          [],
          [
            module,
            function,
            args
          ]
        }]
      ]
    }
  end
end

マクロを展開します。 追加した第三引数に引数の個数を指定する必要がありますが、これを利用することで引数の個数違いの関数を定義することができるようになりました。

defmodule ExYAML do
  require Importer
  import Importer

  import_ruby YAML, :load, 1
  import_ruby YAML, :dump, 1
  import_ruby YAML, :dump, 2
end
iex> ExIRB.start_link(require: ["yaml"])
iex> ExYAML.dump([1, [2, [3, 4]]])
"---\n- 1\n- - 2\n  - - 3\n    - 4\n"
iex> ExYAML.dump([1, [2, [3, 4]]], %{indentation: 4})
"---\n- 1\n-   - 2\n    -   - 3\n        - 4\n"

これで扱いやすくなりました。

しかしまだ何度も import_ruby YAML と書かなければならない手間があります。

Kernel.SpecialForms.import/2 のように、関数名と引数の個数をペアにしたキーワードリストでまとめて定義できるようにしてみます。

マクロを修正します。

defmodule Importer do
  defmacro import_ruby(module, functions) do
    functions
    |> Enum.map(fn {function, arity} ->
      args =
        (?a..?z)
        |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
        |> Enum.take(arity)

      {
        :def, [context: __MODULE__, import: Kernel],
        [
          {
            function,
            [context: __MODULE__],
            args
          },
          [do: {
            {:., [], [ExIRB, :apply]},
            [],
            [
              module,
              function,
              args
            ]
          }]
        ]
      }
    end)
  end
end

利用する側も修正します。

defmodule ExYAML do
  require Importer
  import Importer

  import_ruby YAML, load: 1, dump: 1, dump: 2
end

利用しやすくなりました。

Level 8, Necromancer: モジュールを定義する

しかしまだ問題があります。

defmodule ExYAMLandJSON do
  require Importer
  import Importer

  import_ruby YAML, load: 1, dump: 1
  import_ruby JSON, load: 1, dump: 1
end

同じ名前、同じ引数の数の関数を定義すると、先に定義した関数にすべてのパタンがマッチしてしまい後から定義した関数は呼ばれません。

iex> ExYAMLandJSON.dump(%{a: 1})
"---\n:a: 1\n"

名前空間をわけたいところです。 Ruby のモジュール名を渡しているので、これを名前空間にできないか考えます。

こんどは quote されたモジュールの構造を調べます。

iex> quote do
...>   defmodule Yaml do
...>     def dump(term) do
...>       ExIRB.apply(YAML, :dump, [term])
...>     end
...>
...>     def load(str) do
...>       ExIRB.apply(YAML, :dump, [str])
...>     end
...>   end
...> end
{:defmodule, [context: Elixir, import: Kernel],
 [
   {:__aliases__, [alias: false], [:Yaml]},
   [
     do: {:__block__, [],
      [
        {:def, [context: Elixir, import: Kernel],
         [
           {:dump, [context: Elixir], [{:term, [], Elixir}]},
           [
             do: {{:., [], [{:__aliases__, [alias: false], [:ExIRB]}, :apply]},
              [],
              [
                {:__aliases__, [alias: false], [:YAML]},
                :dump,
                [{:term, [], Elixir}]
              ]}
           ]
         ]},
        {:def, [context: Elixir, import: Kernel],
         [
           {:load, [context: Elixir], [{:str, [], Elixir}]},
           [
             do: {{:., [], [{:__aliases__, [alias: false], [:ExIRB]}, :apply]},
              [],
              [
                {:__aliases__, [alias: false], [:YAML]},
                :dump,
                [{:str, [], Elixir}]
              ]}
           ]
         ]}
      ]}
   ]
 ]}

関数を定義するタプル {:def, ...} の部分を除くと思いの外シンプルです。

また、マクロを利用するモジュールの名前空間の中でモジュールを定義したいわけですが、そのモジュール名は Kernel.SpecialForms.__CALLER__/0 で取得することができます。

これらをすべて適用します。

defmodule Requirer do
  defmacro require_ruby(module, functions) do
    ruby_module =
      case module do
        {:__aliases__, _, [ruby_module]} -> ruby_module
        module -> module
      end

    quoted_functions =
      functions
      |> Enum.map(fn {function, arity} ->
        args =
          (?a..?z)
          |> Stream.map(&{String.to_atom(<<&1>>), [], __MODULE__})
          |> Enum.take(arity)

        {
          :def, [import: Kernel],
          [
            {function, [], args},
            [do: {
              {:., [], [ExIRB, :apply]},
              [],
              [module, function, args]
            }]
          ]
        }
      end)

    {
      :defmodule,
      [import: Kernel],
      [
        Module.concat(__CALLER__.module, ruby_module),
        [do: {:__block__, [], quoted_functions}]
      ]
    }
  end
end

マクロを利用するモジュールを書きます。

defmodule ExYAMLandJSON do
  require Requirer
  import Requirer

  require_ruby YAML, load: 1, dump: 1
  require_ruby JSON, load: 1, dump: 1
end

使ってみます。

iex> ExIRB.start_link(require: ["yaml", "json"])
iex> ExYAMLandJSON.JSON.dump(%{a: 1, b: [1, 2, 3]})
"{\"b\":[1,2,3],\"a\":1}"
iex> ExYAMLandJSON.YAML.dump(%{a: 1, b: [1, 2, 3]})
"---\n:b:\n- 1\n- 2\n- 3\n:a: 1\n"

マクロを利用するモジュールの名前に require_ruby したモジュールの名前を連結した新しいモジュールが定義され、その各々に関数が定義されました。

Level 9, Wizard: ぜんぶまとめると

これらのコードを整理して GitHub に push しました

使ってみましょう。

プロジェクトを作成する

新しいプロジェクトを作成します。プロセスを supervisor で管理したいので --sup オプションを指定します。

$ mix new sample --sup
$ cd sample

依存パッケージを設定する

mix.exs を編集して依存パッケージに ex_irb を指定します。

defmodule Sample.MixProject do
  use Mix.Project

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

  def application do
    [
      extra_applications: [:logger],
      mod: {Sample.Application, []}
    ]
  end

  defp deps do
    [
      {:ex_irb, github: "mattsan/ex_irb"}
    ]
  end
end

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

$ mix deps.get

ExIRB プロセスの起動を設定する

lib/sample/application.ex を編集して ExIRB のプロセスを起動する設定を記述します。

defmodule Sample.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      {ExIRB, require: ["yaml", "json"]}
    ]

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

召喚するモジュールを記述する

ExIRB.Requirer を使って Ruby のモジュールを Elixir のモジュールに召喚します。

defmodule Sample do
  @moduledoc """
  Documentation for Sample.
  """

  require ExIRB.Requirer
  import ExIRB.Requirer

  require_ruby YAML, load: 1, dump: 1, dump: 2
  require_ruby JSON, load: 1, dump: 1
end

使う

iex を起動します。

$ iex -S mix

iex のプロンプトから召喚した Ruby のメソッドを呼びます。 アプリケーションが起動するときに ExIRB を起動しているのですぐに利用することができます。

iex> Sample.JSON.dump(%{a: 1})
"{\"a\":1}"
iex> Sample.YAML.dump(%{a: 1})
"---\n:a: 1\n"
iex> Sample.JSON.load(~S[{"a":1}])
%{"a" => 1}
iex> Sample.YAML.load("---\na: 1\n")
%{"a" => 1}

ちなみに。

終了処理をきちんと書いていないので、 iex を終了すると irb と接続しているポートが閉じて irb が例外を投げスタックトレイスを吐き出して停止します。

まぁ、黒魔術だから…。

結論

わかりやすく書こう。

----うますぎるプログラムはいけない

プログラム書法 第2版

いつか読むはずっと読まない:霊媒師、占い師、奇術師、…

ダンジョンズ&ドラゴンズ スターター・セット第5版

ダンジョンズ&ドラゴンズ スターター・セット第5版