この記事は、Elixir Advent Calendar 2023 シリーズ13 の12日目です
【本コラムは、6分で読め、6分で試せます】
piacere です、ご覧いただいてありがとございます
構造体(struct)は、ユーザー定義のデータ型を作れて便利ですが、内部実装がマップなのに、マップ操作が一部できなかったりと、不便な面もあるので、その攻略法の1つとして、構造体とマップの相互変換についてまとめます
構造体 → マップ
逆説的ですが、構造体をマップに変換してしまえば、構造体の制約は気にしなくて良くなります(ただし、構造体のみ持っているキー制約が効かなくなります)
iex> %Range{first: 1, last: 10, step: 2} |> Map.from_struct
%{first: 1, last: 10, step: 2}
iex> NaiveDateTime.utc_now() |> Map.from_struct
%{
calendar: Calendar.ISO,
day: 1,
hour: 8,
microsecond: {263337, 6},
minute: 25,
month: 1,
second: 52,
year: 2024
}
上記例は、レンジや ~N[~]
等の日時系が実は構造体で出来ているハックでもあるので、下記も通ります
iex> 1..10//2 |> Map.from_struct
%{first: 1, last: 10, step: 2}
iex> ~N[2024-01-01 08:26:31.532047] |> Map.from_struct
%{
calendar: Calendar.ISO,
day: 1,
hour: 8,
microsecond: {532047, 6},
minute: 26,
month: 1,
second: 31,
year: 2024
}
構造体か否か判定
レンジや日時系以外にも、EctoやPhoenix、Nx等で使われているデータ型の多くは、表示上が構造体に見えなくても中身は構造体ですが、下記で判定できます
iex> 1..10//2 |> is_struct
true
iex> ~N[2024-01-01 08:26:31.532047] |> is_struct
true
iex> %{first: 1, last: 10, step: 2} |> is_struct
false
構造体のデバッグ
iex> 1..10//2 |> inspect(structs: false)
"%{__struct__: Range, first: 1, last: 10, step: 2}"
iex> ~N[2024-01-01 08:26:31.532047] |> inspect(structs: false)
"%{__struct__: NaiveDateTime, calendar: Calendar.ISO, day: 1, hour: 8, microsecond: {532047, 6}, minute: 26, month: 1, second: 31, year: 2024}"
iex> %{first: 1, last: 10, step: 2} |> inspect(structs: false)
"%{first: 1, last: 10, step: 2}"
このテクニックは、書籍「プログラミングElixir」 のP332(第一版だとP268)で解説されています
マップ → 構造体
野良のマップを構造体にコンバートしたり、構造体が持っているキー制約を効かせたいときは、下記で変換できます
iex> struct(Range, %{first: 1, last: 10, step: 2})
1..10//2
iex> struct(Range, %{})
nil..nil//nil
iex> %{first: 1, last: 10, step: 2} |> then(& struct(Range, &1))
1..10//2
iex> struct(NaiveDateTime, %{year: 2024, month: 1, day: 1, hour: 8, minute: 26, second: 31})
~N[2024-01-01 08:26:31]
構造体に存在しないキーは切り捨てられます
iex> struct(Range, %{first: 1, last: 10, step: 2, no_exist: "No Range, No Life"})
1..10//2
iex> struct(Range, %{first: 1, last: 10, step: 2, no_exist: "No Range, No Life"}) |> inspect(structs: false)
"%{__struct__: Range, first: 1, last: 10, step: 2}"
NaiveDateTime
は second
まで削るとエラーになりますが、これは構造体自体の仕様では無く、各構築時のチェックになります
iex> struct(NaiveDateTime, %{year: 2024, month: 1, day: 1, hour: 8, minute: 26})
#Inspect.Error<
got FunctionClauseError with message:
"""
no function clause matching in Calendar.ISO.time_to_string/5
"""
その証拠に、レンジ等では型に適合しなそうな場合でも、エラーにはならず、ムリやり構築されます
iex> struct(Range, %{first: "This is first", last: 10, step: 2})
"This is first"..10//2
構築はされるものの、マトモに使える状態では無いです
iex> struct(Range, %{first: 1, last: 10, step: 2}) |> Enum.to_list
[1, 3, 5, 7, 9]
iex> struct(Range, %{first: "This is first", last: 10, step: 2}) |> Enum.to_list
[]