【TIPS】構造体（struct）とマップの相互変換

この記事は、[Elixir Advent Calendar 2023 シリーズ13](https://qiita.com/advent-calendar/2023/elixir) の12日目です

---

【本コラムは、6分で読め、6分で試せます】

[piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:

構造体（struct）は、ユーザー定義のデータ型を作れて便利ですが、内部実装がマップなのに、マップ操作が一部できなかったりと、不便な面もあるので、その攻略法の1つとして、構造体とマップの相互変換についてまとめます

# 構造体 → マップ

逆説的ですが、構造体をマップに変換してしまえば、構造体の制約は気にしなくて良くなります（ただし、構造体のみ持っているキー制約が効かなくなります）

```diff_elixir
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[～]` 等の日時系が実は構造体で出来ているハックでもあるので、下記も通ります

```diff_elixir
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等で使われているデータ型の多くは、表示上が構造体に見えなくても中身は構造体ですが、下記で判定できます

```diff_elixir
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
```

# 構造体のデバッグ

```diff_elixir
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」**](https://www.amazon.co.jp/exec/obidos/ASIN/4274226379/onelinebiz-22) のP332（第一版だとP268）で解説されています

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/73019c11-dc0d-9900-e06b-1d188240a4ae.png)](https://www.amazon.co.jp/exec/obidos/ASIN/4274226379/onelinebiz-22)

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a4e2076c-8106-51d1-ab0a-59f63dc59a2e.png)](https://www.amazon.co.jp/exec/obidos/ASIN/4274219151/onelinebiz-22)

# マップ → 構造体

野良のマップを構造体にコンバートしたり、構造体が持っているキー制約を効かせたいときは、下記で変換できます

```diff_elixir
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]
```

構造体に存在しないキーは切り捨てられます

```diff_elixir
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` まで削るとエラーになりますが、これは構造体自体の仕様では無く、各構築時のチェックになります

```diff_elixir
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
      """
```

その証拠に、レンジ等では型に適合しなそうな場合でも、エラーにはならず、ムリやり構築されます

```diff_elixir
iex> struct(Range, %{first: "This is first", last: 10, step: 2})
"This is first"..10//2
```

構築はされるものの、マトモに使える状態では無いです

```diff_elixir
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
[]
```