7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 9

Elixirチートシートを作ろう、番外編その8 ETSに入門

Posted at

1. OverView

うん、チートシートもあるけど、並行して色々使わないと忘れる。
もうmap忘れてた!忘れて、データの管理システムとしてETSを頑張ろう!と思った次第。

本日は、ETSでご機嫌を伺おうかと思います。

Erlang Term Storage (ETS)はOTP内部に組み込まれている強力なインメモリストレージエンジンで、
非常に大きなデータセットを扱える様です。

ほかにも、DETS(Disk-based Erlang Term Storage)と言うディスクを使用してデータをディスクに永続化するバージョンもありますが、とりあえず、ETSをやっていきたいと思います。

今回は、さくっとElixir School準拠でやって行きますね。

2. まずは解説

2.1 テーブルの作成

:etc.newでまずはテーブルを作ります。

new(Name, Options) -> tid() | atom()
Name = atom()
Options = [Option]

Optinsは解説するにはまだ調べてないので、デカいことを言えませんな。

iex(4)> table = :ets.new(:user_lookup, [:set, :protected])
#Reference<0.614714515.3794403338.130351>
iex(5)> table = :ets.new(:user_lookup, [:set, :protected, :named_table])
:user_lookup

protected -
The owner process can read and write to the table. Other processes can only read the table. This is the default setting for the access rights.

set
The table is a set table: one key, one object, no order among objects. This is the default table type.

オプションに:named_tableが追加されている事に注意してください。

一つ目の書き方だと、:ets.newの戻り値にバインドされた変数、tableを介してアクセスするのですが、
二つ目の書き方だと、名前である:user_lookupでアクセス出来る様です。

なので、このサンプルでは後者で行きます。

さて、今、ビビっているので原文で僕の気持ちを共有してください。

ETSはスキーマを持ちません。唯一の制限は、データは最初の項がキーであるタプルとして格納されなくてはいけないというものです。

フリーダムだなぁ。後で、検索を読んでから、どこまで自由か、後ほど試してみましょうか。

では、insertのサンプル

iex(6)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true

次、更新…いや、INSERTか?
と言うのも、insert/2をテーブル作成時にset, ordered_setを指定したテーブルに対して実行すると、keyが同じ場合は上書きされちゃうそうです。

ちょっと早いですが、lookup/2を使って、中身を見てみましょう。

iex(6)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true
iex(7)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]
iex(8)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "JavaScript"]})
true
iex(9)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "JavaScript"]}]

updateが要らんのは良いけど、バグりそう…まぁ、なので、ちゃんと対策があります。
キーが存在している場合に false を返してくれる insert_new/2 があるのです。

iex(10)> :ets.insert_new(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
false
iex(11)> :ets.insert_new(:user_lookup, {"3100", "", ["Elixir", "Ruby", "JavaScript"]})
true
iex(12)> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "JavaScript"]}]
iex(13)> :ets.lookup(:user_lookup, "3100")
[{"3100", "", ["Elixir", "Ruby", "JavaScript"]}]

:ets.insert_new/2では、重複時にfalseが返り、失敗するので、安心ですね。

2.2 データのマッチ

さて、お待ちかね検索となります。
もう、上で使ってしまいましたが、順に、「俺が納得するまで」弄り倒しましょう。
…ごめん、つきあって、

2.2.1 キー探索(:ets.lookup)

上で例が出ておりますね。:ets.lookup/2です。

:ets.lookup(テーブル名, KEY)

…第一引数がテーブル名で済むのは、テーブル作成時にオプションに:named_tableを設定しているからですね。

iex(2)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true
iex(3)> :ets.insert(:user_lookup, {"doomspork2", "mokemoke", ["Elixir", "Ruby", "Java"], "beginner"})
true
iex(4)>  :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]
iex(5)> :ets.lookup(:user_lookup, "doomspork2")
[{"doomspork2", "mokemoke", ["Elixir", "Ruby", "Java"], "beginner"}]
iex(6)>

ついでなんで、しれっと混ぜておりますが、前述の「ETSはスキーマを持ちません」って話は間違いないですね。
tableをsetに設定した場合、keyに対して、タプルを一つ格納してくれるDBって捉えるのがよいのかもしれません。

2.2.2 単純なマッチ(:ets.match/2)

さぁ、皆さんご一緒に、「どこが単純じゃ!」
やってみたら単純でした。

マッチ内で変数(variable )を指定するには、 :"$1" 、 :"$2" 、 :"$3" などのアトムを用います。
変数の数字は結果の位置を示していて、マッチの位置ではありません。
興味のない値については、 :_ 変数を用います。

:"$1"や:_が変数ですね。

そう考えると、単純タプルをマッチしてくれている様です。

iex(4)>  :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]
iex(6)> :ets.match(:user_lookup, {:"$1", "Sean", :_})
[["doomspork"]]

"$1"だけが返っていますね。

iex(16)> :ets.lookup(:user_lookup, "doomspork2")
[{"doomspork2", "mokemoke", ["Elixir", "Ruby", "Java"], "beginner"}]
iex(17)> :ets.match(:user_lookup, {:"$1", "mokemoke", :"$3" })
[] <-タプルがマッチしてないので、検索結果が返ってこない。
iex(18)> :ets.match(:user_lookup, {:"$1", "mokemoke", :"$3", :"$4" })
[["doomspork2", ["Elixir", "Ruby", "Java"], "beginner"]]
iex(21)> :ets.match(:user_lookup, {:"$3", "mokemoke", :_, :_ })
[["doomspork2"]]

どうやら、:"$[1-9}"と言うタプルの数字は、変数を表すので数字はなんも良いようです。
これは、Erlangを学ばんとダメかなー。

次に、match_object/2となります。
こちらは、matchはマッチした変数(結果)だけを返しましたが、こちらは全体を返してくれます。

iex(4)> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"})
[
  {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
  {"3100", "", ["Elixir", "Ruby", "JavaScript"]}
]
iex(5)>  :ets.match(:user_lookup, {:"$1", :_, :"$3"})
[
  ["doomspork", ["Elixir", "Ruby", "Java"]],
  ["3100", ["Elixir", "Ruby", "JavaScript"]]
]

違いがわかるでしょうか?"Sean"が、match_objectの中に入ってますね。
ets.match_objectはレコード全体を返してくれています。

2.2.3 発展的な探索(select/2)

select/2を紹介する前に、まずは、テストデータを入れ替えますね。

iex(10)> :ets.insert_new(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
false
iex(11)> :ets.insert_new(:user_lookup, {"3100", "", ["Elixir", "Ruby", "JavaScript"]})
true

まずは、:ets.match_objectで検索してみましょう。
2レコードとも、オブジェクトが返っていることにご注意。

iex(6)> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"})
[
  {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
  {"3100", "", ["Elixir", "Ruby", "JavaScript"]}
]

では、select/2です。

select(Tab, MatchSpec, Limit) ->
{[Match], Continuation} | '$end_of_table'

第二引数が、LISTになっているので、ご注意ください。

iex(7)> :ets.select(:user_lookup, [{{:"$1", :_, :"$3"}, [], [:"$_"]}])
[
  {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
  {"3100", "", ["Elixir", "Ruby", "JavaScript"]}
]

引数のMatchSpecは、LIST形式で、以下の三つを与えます。
[{match_pattern(), [term()], [term()]}]

詳しくは以下を参照。
https://www.erlang.org/docs/23/man/ets#type-match_spec

LIST内部。一つ目の引数はmatch_pattern()…これは上でさんざんやった{:"$1", :_, :"$3"}ですね。
次は、ガード条件、今回は空リスト
今回は三つ目の引数で、戻り値を指定できます。
今まで使った"$1"に加え、以下のものが指定出来ます。

:"$$" 結果をリストで返す
:"$_"  結果は元のデータオブジェクトで返す。

二つ目のガード条件を弄ってみましょう。

iex(14)> :ets.select(:user_lookup, [{{:"$1", :_, :"$3"}, [{:"==", :"$1", "doomspork"}], [:"$$"]}])
[["doomspork", ["Elixir", "Ruby", "Java"]]]

GUARD節で、条件が変わったのを確認できるでしょうか?
と、複雑なので次の項でfun2ms/1により簡潔な記述を試みます。

2.2.4 発展的な探索を簡潔にしてみる(select/2とfun2ms/1)

さて、先ほど話した検索条件、match_specを、関数で書く方法があります。

:ets.fun2msで、匿名変数で条件が置き換えられている事にご注意ください。

iex(19)> fun = :ets.fun2ms(fn {username, _, langs} when length(langs) > 2 -> username end)
[{{:"$1", :_, :"$2"}, [{:>, {:length, :"$2"}, 2}], [:"$1"]}]
iex(20)> :ets.select(:user_lookup, fun)
["doomspork", "3100"]

抜き出してみますね?

    fn {username, _, langs} when length(langs) > 2 -> username end

条件も戻り値も ->の先で指定しておりますな。

2.3 レコードの除去(:ets.delete/2)

:ets.delete/2で、除去した条件を削除します。

iex(23)> :ets.delete(:user_lookup, "doomspork")
true
iex(24)>

2.4 テーブルの除去(:ets.delete/1)

:ets.delete/1で、テーブルごと削除します。

iex(24)> :ets.delete(:user_lookup)
true

3. 本日のチートシート

うーん、もうちょいmarkdownを調べないと、読みにくいんだよなぁ。
とりあえず、書いときますね。

やりたいこと
テーブルの作成 iex(5)> table = :ets.new(:user_lookup, [:set, :protected, :named_table])
:user_lookup
テーブルの挿入 iex(6)> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true
テーブルの更新 insertをすると、同じキーを持っているレコードが置き換えられる
テーブルを更新せずに挿入 iex(10)> :ets.insert_new(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
データの検索
キー探索 iex(4)> :ets.lookup(:user_lookup, "doomspork")
単純なマッチ :ets.match/2を使用する
iex(6)> :ets.match(:user_lookup, {:"$1", "Sean", :_})
:ets.match_objectを使用する。オブジェクト(レコード)全体が返る
iex(4)> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"})
発展的な探索 iex(7)> :ets.select(:user_lookup, [{{:"$1", :, :"$3"}, [], [:"$"]}])
fun2ms/1を使うと、match_specを関数で記述出来る
iex(19)> fun = :ets.fun2ms(fn {username, _, langs} when length(langs) > 2 -> username end)
レコードの除去 iex(23)> :ets.delete(:user_lookup, "doomspork")
テーブルの除去 :ets.delete/1で、テーブルごと削除します。
iex(24)> :ets.delete(:user_lookup)
7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?