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

# 1. OverView

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

本日は、ETSでご機嫌を伺おうかと思います。
    
https://elixirschool.com/ja/lessons/storage/ets

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

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

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


# 2. まずは解説

## 2.1 テーブルの作成

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


https://www.erlang.org/docs/23/man/ets#new-2

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)

https://elixirschool.com/ja/lessons/storage/ets#%E5%8D%98%E7%B4%94%E3%81%AA%E3%83%9E%E3%83%83%E3%83%81-8

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

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