なぜTupleを保存?
Exlixir勉強中です。
勉強用にオセロゲームを作ってみました。盤面はTupleで保存しています。
タプルの中身は、:none, :black , :whiteのいずれか。これで盤面を表しています。
Ectoで盤面を保存して、棋譜の履歴を保存を実現したい。
↓
Tupleを保存したい。
Tupleを使わず、ListやMapで盤面を保存していれば、型を作らなくても問題ないと思いますが、勉強も兼ねて、Tupleに挑戦してみます。
実はこんな方法もあるよ。などコメントいただけると嬉しいです。
修正前
かなり、ゴチャゴチャです。
盤面はBoard.cellsにTuple型で保存されています。
Tupleを含んでいるBoardは、changesetに受け付けられないので、Tuple.to_list でリストに変換。
Atomを数値に変換するconvert_cell_to_integer()かませてます
def push(board = %Board{}) do
id = board.id
cells = Tuple.to_list(board.cells)
|> Enum.map(&convert_cell_to_integer(&1))
board_map = %{board | cells: cells, id: id, turn: convert_cell_to_integer(board.turn)}|> Map.from_struct()
new_data = changeset(%__MODULE__{},board_map)
from(log in __MODULE__, where: log.id >= ^id) |> Repo.delete_all
if id > 0, do: Repo.insert(new_data)
board
end
Ecto.Typeを使ってEctoTupleAtom
型を作り、変換処理をEcto側に持っていきます。
Ecto.Type/ParameterizedTypeの試行錯誤
本家のドキュメントを見て簡単にできるかと思ったんですが、castってなに?dumpとどんな関係なの?という疑問で、ハマりました。この点をまとめておきます。
callback | 入力→出力 | 例 | コメント |
---|---|---|---|
cast() | 任意の型→dumpが受け付ける型 | Tuple→Tuple | dumpが受け付ける型に変換(前処理)。受け付けない型は拒否 |
dump() | castが出力する型→Ecto native | Tuple→:string | DBに書き込むデータに変換する |
load() | Ecto native→任意の型 | :string→Tuple | DBから読んだ生の値を、要望の型に変換する |
Ecto nativeの型は、フィールドに設定されている型(:integerや、:string等)と考えてよいかと思います。
今回は、castでは、何も処理しませんでした(tupleであるかの確認だけ)が、listでもtupleでも受け付けるようにする場合、listを受け取り、tupleで出力するcastを追加すれば両方に対応できます。
基本、cast dumb loadのcallbackを書くだけなので、この役割分担がわかれば、あとは一直線でした。
Ecto.ParameterizedTypeの実装
Ecto.TypeとEcto.ParameterizedTypeの違いは、パラメータがあるか無いかの違いです。
atomの変換をパラメータとし持たせたかったので、Ecto.ParameterizedTypeを使います。
パラメータを受け付ける、init()追加で必要になります。
EctoTupleAtomの実装は次の通りです。
defmodule EctoTupleAtom do
use Ecto.ParameterizedType
def type(_params), do: :string
def init(opts) do
forward_map = Enum.into(opts[:values], %{})
reverce_map = Map.new(forward_map, fn {key, val} -> {val, key} end)
%{f_map: forward_map, r_map: reverce_map}
end
def cast(cells, _params) when is_tuple(cells) do
{:ok, cells}
end
def load(data, _loader, params) when is_binary(data) do
{:ok,
data
|> String.split("", trim: true)
|> Enum.map(fn x -> params.r_map[x] end)
|> List.to_tuple()}
end
def dump(cells, _dumper, params) when is_tuple(cells) do
{:ok,
cells
|> Tuple.to_list()
|> Enum.map(fn x -> params.f_map[x] end)
|> Enum.join("")}
end
def dump(nil, _, _), do: {:ok, ""}
def dump(_, _, _), do: :error
end
Schema
EctoTupleAtomをschemaに組み込んで使ってみます。valuesにAtomと対応する文字のリストを指定します。
defmodule HexReversi.BoardHistory.Log do
use Ecto.Schema
import Ecto.Changeset
schema "logs" do
field(:xs, :integer)
field(:ys, :integer)
field(:cells, EctoTupleAtom, values: [none: "0", black: "1", white: "2"])
field(:gameover, :boolean)
field(:turn, Ecto.Enum, values: [none: "0", black: "1", white: "2"])
field(:white, :integer)
field(:black, :integer)
timestamps()
end
@doc false
def changeset(log, attrs) do
log
|> cast(attrs, [:xs, :ys, :cells, :gameover, :turn, :white, :black, :id])
|> validate_required([:cells])
end
end
変換の様子
dumpとloadにIO.Inspectでデータを入れて、データの変換の様子をみてみました。
実行した結果は以下の通りです。
atomを文字に変換してつなげたものになっています。
dump_in: {:none, :none, :black, :white}
dump_out: "0012"
load_in: "0012"
load_out: {:none, :none, :black, :white}
EctoTupleAtom適用後のプログラム
データの変換処理が不要になり、すっきりできました。
pushでは、delete()とinsert()を行てたので、分けてみました。
修正前に比べて、かなり見通しが良くなりました。
def push(board = %Board{}) do
%{board | id: board.id + 1}
|> delete()
|> insert()
end
def delete(board = %Board{}) do
id = board.id
from(log in Log, where: log.id >= ^id) |> Repo.delete_all()
board
end
def insert(board = %Board{}) do
new_data = Log.changeset(%Log{}, Map.from_struct(board))
if board.id > 0, do: Repo.insert(new_data)
board
end
Ecto.Enumについて
当初、atomをDBで保存処理を、Ecto.Enumを参考にしてできないか検討しました。
Ecto.Enumのソースを調べました。
残念ながら、うまくこの機能を持ってくることはできませんでしたが、cast/dumpがどのように使うか参考になりました。
cast
基本はAtomが入力になる
Atom以外にvalue(変換後の値)や Atom.to_string(key)も受け付ける。
想定外の入力があった場合には、これをAtomに変換する処理するcastを作成しています。
for {key, value} <- opts, k <- Enum.uniq([key, value, Atom.to_string(key)]) do
def cast(unquote(k)), do: {:ok, unquote(key)}
end
dump
atomから目的の値に変換するdumpを作成しています。
for {key, value} <- opts, k <- Enum.uniq([key, value, Atom.to_string(key)]) do
def dump(unquote(k)), do: {:ok, unquote(value)}
end
値の変換のメインはdump。castは前処理という位置づけ
まとめ
Tupleを保存する型を作ることで、データの保存処理が簡素に書けるようになった。
参考
https://hexdocs.pm/ecto/Ecto.Type.html
https://hexdocs.pm/ecto/Ecto.ParameterizedType.html
https://hexdocs.pm/ecto/Ecto.Enum.html
https://github.com/gjaldon/ecto_enum