疑問
なぜ、アトム型のキーを持つマップにおいて、値を参照するときにキーをstring型のように扱う場合があるのか?
以下のようなタイプスペック時にq_task_time
のキーであるstart_datetime
はアトムであるはずですが、string型のような参照の仕方をします。
疑問に思ったので、ドキュメントと照らし合わせながら理由を紐解いていきました。
タイプスペックは以下
@type q_task_time :: %{
start_datetime: NaiveDateTime.t() | nil,
end_datetime: NaiveDateTime.t() | nil
}
def update_q_task_times(%__MODULE__{id: id} = editor, ans_editor_id, :start)
when id == ans_editor_id do
q_task_times = List.insert_at(editor.q_task_times, 0, %{"start_datetime" => now(), "end_datetime" => nil})
editor = %{editor | q_task_times: q_task_times}
{:ok, %Ans{id: ans_id}} =
Answers.save_user_mb_ques_answer(editor.ans, Map.from_struct(editor))
%{editor | ans: Answers.get_user_mb_ques_answer(ans_id)}
end
理由
Postgres(DB)ではアトムは許容しないためStringに変換されるから
マップ キーはアトムではなく文字列または整数にすることをお勧めします。マップのシリアル化方法によってはアトムが受け入れられる場合もありますが、セキュリティ上の理由から、データベースは常にアトム キーを文字列に変換します。
参考:hexdoc ecto.schema module-primary-keys
疑問
タイプスペックのキーはなぜString
型で書かれていないのか?
以下タイプスペックにおいて、マップをアトムのキーで書く書き方がOKですが、String
のキーで書く書き方はKernel.TypespecError
が出ます。
OKな書き方
@type q_task_time :: %{start_datetime: NaiveDateTime.t() | nil, end_datetime: NaiveDateTime.t() | nil}
@type q_task_time :: %{:start_datetime => NaiveDateTime.t() | nil, :end_datetime=> NaiveDateTime.t() | nil}
NGな書き方
@type q_task_time :: %{"start_datetime" => NaiveDateTime.t() | nil, "end_datetime"=> NaiveDateTime.t() | nil} # Kernel.TypespecError
理由
文字列のキーの型定義ができないから
明示的には書かれていませんが、型スペックでマップのキーとして使えるのは主にアトムや整数であることが暗黙の了解となっています。
理由はチェックやパターンマッチングの際に効率的に処理できるためです。
[参考: hexdoc typespecs reference]
(https://hexdocs.pm/elixir/1.16.3/typespecs.html)
基本の復習
マップとは
- キーにアトムを強制されない
- 要素の順が保証されない
- キーがアトムだった場合のみ呼び出し時に
.
(ドット演算子)が使える -
=>
ではなく:
を使った書き方の場合、キーがアトムの場合:
は省略できる
map = %{:apple => 200, "orange" => 400}
map.apple
map["orange"]
map2 = %{banana: 200}
map2[:banana]
map2.banana
タイプスペックとは
Elixirのタイプスペック(typespec)は、関数やデータ構造の型情報を明示するための仕様です。タイプスペックを使うことで、コードの可読性と信頼性を向上させ、ドキュメント生成や静的解析ツールの活用が容易になります。
Elixirは動的型付け言語ですが、タイプスペックを使うことで静的型付け言語のように扱えます。雑に例えるとJavaScriptからTypeScriptへの変換のようなものです。
例1. 型エイリアスの定義
@type user_id :: integer
@type user :: %{
id: user_id,
name: String.t(),
email: String.t()
}
例2.関数のタイプスペック
@spec add(integer, integer) :: integer
def add(a, b) do
a + b
end
@spec get_user(user_id) :: user | nil
def get_user(id) do
# ユーザーを取得するロジック
end
例3.モジュールのタイプスペック
defmodule UserManager do
@type user_id :: integer
@type user :: %{
id: user_id,
name: String.t(),
email: String.t()
}
@spec create_user(user_id, String.t(), String.t()) :: user
def create_user(id, name, email) do
%{
id: id,
name: name,
email: email
}
end
@spec get_user_name(user) :: String.t()
def get_user_name(user) do
user.name
end
end
まとめ
ややこしですがEcto
とtypespec
とは別物です。
Typespec
の目的は型の解析と静的型チェックEcto
の目的はデータベースのデータを扱うためのライブラリです。