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

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

ETS (Erlang Term Storage)を利用する(今後は、たぶん)

ETS とは

Erlang/OTP には ETS というストレージライブラリが標準で用意されています。 当然 Elixir でも利用できます。

www.erlang.org elixirschool.com

ただ、ETS の問い合わせには match specification と呼ばれる構造を利用する必要があります。

www.erlang.org

たとえば。 「三つ組のタプルの、3 番目の値が 2 より大きいものの、1 番目のデータを集める」 という問い合わせをしたい場合、問い合わせには次のようなデータを用意しなければなりません。

[{{:"$1", :_, :"$2"}, [{:>, {:length, :"$2"}, 2}], [:"$1"]}]

これは構造としては次のような関数と同じです。

fn {a1, _, a2} when a2 > 2 -> a1 end

こうして見せられればなるほどとわかるのですが、最初から迷わずに書ける自信がありません。

個人的にはこの記述をするのがおっくうで、 データの格納先に ETS を利用することはあまり考えていませんでした。

それが最近になって ets:fun2ms/1 という関数の存在を知りました。 昔からある関数なのですが、ETS を利用する頭がなかったので、そのような便利なものがあることも知らず過ごしていた始末。

iex(1)> :ets.fun2ms(fn {a1, _, a2} when a2 > 2 -> a1 end)
[{{:"$1", :_, :"$2"}, [{:>, :"$2", 2}], [:"$1"]}]

よし、これで使える! …と思ったのも束の間。

IEx 上では利用できるものの、モジュールに組み込んで実行するとエラーになってしまいます。

defmodule Foo do
  def run do
    :ets.fun2ms(fn {a1, _, a2} when a2 > 2 -> a1 end)
  end
end

Foo.run() |> IO.inspect()
$ elixir foo.exs
** (exit) exited in: :ets.fun2ms(:function, :called, :with, :real, :fun, :should, :be, :transformed, :with, :parse_transform, :or, :called, :with, :a, :fun, :generated, :in, :the, :s
hell)
    ** (EXIT) :badarg
    (stdlib 3.17.1) ets.erl:608: :ets.fun2ms/1
    foo.exs:7: (file)
    (elixir 1.13.3) lib/code.ex:1183: Code.require_file/2

詳しい理由とかは、この記事を読んでいただくとして。

elixirforum.com

悲しい気分で記事を読み進んでいたわけですが。

実は関数から match specification へ変換する Elixir 製のパッケージが提供されているとのこと。 これもけっこう昔から。

Ex2ms

hex.pm

Ex2ms には fun/1 というマクロが 1 つ定義されています。 import すれば、ほぼ関数と同じ書き方で match specification を取得することができます。

iex(1)> Mix.install([:ex2ms])
:ok
iex(2)> import Ex2ms
Ex2ms
iex(3)> fun do {a1, _, a2} when a2 > 2 -> a1 end
[{{:"$1", :_, :"$2"}, [{:>, :"$2", 2}], [:"$1"]}]

match specification を簡単に取得する方法が見つかった以上、ETS を使わない理由がなくなりました。

と、いうわけで、さっそく書いてみました。

ETS を Ex2ms で使う

気象庁の過去の気象データ検索を利用して、東京の 2022 年 2 月の日々の気温データを取得しました。

www.data.jma.go.jp

このデータを ETS のテーブルに格納して、いろいろ検索してみます。

Mix.install([:ex2ms])

defmodule JMA do
  import Ex2ms


  # [気象庁|過去の気象データ検索](https://www.data.jma.go.jp/obd/stats/etrn/index.php) で取得した
  # 東京の 2022年2月の1日ごとの平均気温,最高気温,最低気温
  @temperatures [
    {~D[2022-02-01], 5.6, 11.2,  1.1},
    {~D[2022-02-02], 5.5, 11.1,  0.9},
    {~D[2022-02-03], 5.8, 11.8,  0.6},
    {~D[2022-02-04], 4.9,  8.5,  2.4},
    {~D[2022-02-05], 3.5,  9.2,  0.3},
    {~D[2022-02-06], 2.3,  8.2, -1.9},
    {~D[2022-02-07], 4.5,  9.7, -0.5},
    {~D[2022-02-08], 5.3,  9.1,  2.2},
    {~D[2022-02-09], 6.2, 10.8,  1.8},
    {~D[2022-02-10], 2.3,  6.1,  0.6},
    {~D[2022-02-11], 4.1,  9.2,  0.7},
    {~D[2022-02-12], 4.7,  9.9,  0.2},
    {~D[2022-02-13], 3.1,  5.0,  0.8},
    {~D[2022-02-14], 3.9,  8.0,  0.8},
    {~D[2022-02-15], 5.5, 11.6,  1.1},
    {~D[2022-02-16], 6.0, 11.8,  1.3},
    {~D[2022-02-17], 4.6,  9.7,  0.5},
    {~D[2022-02-18], 5.7, 11.4, -0.2},
    {~D[2022-02-19], 5.0,  8.9,  1.5},
    {~D[2022-02-20], 5.9, 10.0,  2.4},
    {~D[2022-02-21], 4.1,  8.7,  1.2},
    {~D[2022-02-22], 4.4, 10.0, -0.5},
    {~D[2022-02-23], 4.4, 10.3,  0.1},
    {~D[2022-02-24], 4.8, 10.1,  1.7},
    {~D[2022-02-25], 6.5, 13.4, -0.1},
    {~D[2022-02-26], 8.6, 14.7,  3.0},
    {~D[2022-02-27], 9.5, 18.5,  3.2},
    {~D[2022-02-28], 9.8, 15.9,  4.5}
  ]

  def run do
    # テーブルを作成する
    tid = :ets.new(:jma, [])

    # 気温データを投入する
    @temperatures
    |> Enum.each(&:ets.insert(tid, &1))

    IO.puts("\n最低気温が氷点下の日のデータ")
    :ets.select(tid, fun do {_date, _avg, _max, min} = t when min < 0 -> t end)
    |> IO.inspect()

    IO.puts("\n最高気温と最低気温の差が15度を超える日のデータ")
    :ets.select(tid, fun do {date, _avg, max, min} when (max - min) > 15 -> {date, max, min, max - min} end)
    |> IO.inspect()

    IO.puts("\n最高気温と最低気温の差が15度を超える日のデータ(結果をキーワードリストで取得する)")
    :ets.select(tid, fun do {date, _avg, max, min} when (max - min) > 15 -> [date: date, max: max, min: min, diff: max - min] end)
    |> IO.inspect()
  end
end

JMA.run()

実行。

$ elixir jma.exs

最低気温が氷点下の日のデータ
[
  {~D[2022-02-06], 2.3, 8.2, -1.9},
  {~D[2022-02-22], 4.4, 10.0, -0.5},
  {~D[2022-02-07], 4.5, 9.7, -0.5},
  {~D[2022-02-18], 5.7, 11.4, -0.2},
  {~D[2022-02-25], 6.5, 13.4, -0.1}
]

最高気温と最低気温の差が15度を超える日のデータ
[{~D[2022-02-27], 18.5, 3.2, 15.3}]

最高気温と最低気温の差が15度を超える日のデータ(結果をキーワードリストで取得する)
[[date: ~D[2022-02-27], max: 18.5, min: 3.2, diff: 15.3]]

なかなかいい感じ。

いまさらながら ETS の便利さを感じたできごとでした。

いつか読むはずっと読まない:先入観

ETS を使わなければならない状況にならなかったというのもあってか、「問い合わせを書くのが面倒だなぁ」という思い込みを持ったままずっと来てしまいました。

そう感じるのは、たいていは、自分だけではないはずなので、それを解消する方法がすでにあるはず、と思い至ればよかったのですが。 一度思い込んでしまうと、なかなかそれを払拭できないものですね。