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
@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 {, , , min} = t when min < 0 -> t end)
|> IO.inspect()
IO.puts("\n最高気温と最低気温の差が15度を超える日のデータ")
:ets.select(tid, fun do {date, , max, min} when (max - min) > 15 -> {date, max, min, max - min} end)
|> IO.inspect()
IO.puts("\n最高気温と最低気温の差が15度を超える日のデータ(結果をキーワードリストで取得する)")
:ets.select(tid, fun do {date, , 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 を使わなければならない状況にならなかったというのもあってか、「問い合わせを書くのが面倒だなぁ」という思い込みを持ったままずっと来てしまいました。
そう感じるのは、たいていは、自分だけではないはずなので、それを解消する方法がすでにあるはず、と思い至ればよかったのですが。
一度思い込んでしまうと、なかなかそれを払拭できないものですね。