【TIPS】構造体（struct）に使える関数／使えない関数（エラー出ないけど挙動がデタラメな関数も）

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

---

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

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

構造体は、内部実装がマップなのに、マップ操作が一部できないですが、何ができて、何ができないかを確認します

# Mapモジュール

## 値の更新 … 問題無し

なお、`Map.replace` や `Map.intersect`、`Map.merge` 等も同様です

```diff_elixir
iex> 1..10//2 |> Map.put(:first, 2)
2..10//2
iex> 1..10//2 |> Map.put(:first, 2) |> Enum.to_list
[2, 4, 6, 8, 10]
```

## 構造体に無いキー／バリューの追加 … 無視される

構築時と同様です

```diff_elixir
iex> 1..10//2 |> Map.put(:no_exist, "I'm not Range")
2..10//2
```

## <font color="red">値の削除 … NG</font>

レンジは、`inspect` でエラーが出ていました

なお、`Map.drop` だけで無く、`Map.delete` や `Map.pop`、`Map.reject`、`Map.split` も同様です

```diff_elixir
iex> 1..10//2 |> Map.drop([:first])
#Inspect.Error<
  got FunctionClauseError with message:

      """
      no function clause matching in Inspect.Range.inspect/2
      """

  while inspecting:

      %{__struct__: Range, last: 10, step: 2}

  Stacktrace:
…
```

NaiveDateTimeでもエラーです … 構築時は雑で良かったのに、削除時は厳しいですね

```diff_elixir
iex> NaiveDateTime.utc_now() |> Map.drop([:microsecond])
NaiveDateTime.utc_now() |> Map.drop([:microsecond]) 
#Inspect.Error<
  got MatchError with message:

      """
      no match of right hand side value: %{__struct__: NaiveDateTime, calendar: Calendar.ISO, day: 1, hour: 10, minute: 19, month: 1, second: 26, year: 2024}
      """

  while inspecting:

      %{
        __struct__: NaiveDateTime,
        calendar: Calendar.ISO,
        day: 1,
        hour: 10,
        minute: 19,
        month: 1,
        second: 26,
        year: 2024
      }

  Stacktrace:
…
```

ということで、マップへの変換と構造体の再構築とセットにするなら、いけます

```diff_elixir
iex> 1..10//2 |> Map.from_struct |> Map.drop([:first]) |> then(& struct(Range, &1))      
nil..10//2

iex> NaiveDateTime.utc_now() |> Map.from_struct |> Map.drop([:microsecond]) |> then(& struct(NaiveDateTime, &1))
~N[2024-01-01 10:21:26]
```

## リスト化 … 問題無し

```diff_elixir
iex> 1..10//2 |> Map.to_list        
[__struct__: Range, first: 1, last: 10, step: 2]
```

## キー取得 … 問題無し

構造体用キーも含まれるので、除外を忘れずに

```diff_elixir
iex> 1..10//2 |> Map.keys 
[:__struct__, :first, :last, :step]

iex> 1..10//2 |> Map.keys |> List.delete(:__struct__)  
[:first, :last, :step]
```

## 値取得 … 問題無し

構造体名も含まれるので、除外を忘れずに

```diff_elixir
iex> 1..10//2 |> Map.values 
[Range, 1, 10, 2]

iex> 1..10//2 |> Map.values |> List.delete(Range)
[1, 10, 2]
```

# <font color="red">Enumモジュール … 動かない or 動くがデタラメ</font>

## <font color="red">カウント … 動くがデタラメ</font>

レンジのカウントは、4が期待値なので、エラーは出ないけど使わない方が良さそうです

```diff_elixir
iex> 1..10//2 |> Enum.count
5
```

`NaiveDateTime` は、`Enumerable` では無いため、全面的に `Enum` 利用がNGっぽいです（以降、省略）

```diff_elixir
iex> NaiveDateTime.utc_now() |> Enum.count
** (Protocol.UndefinedError) protocol Enumerable not implemented for ~N[2024-01-01 10:41:27.025257] of type NaiveDateTime (a struct). This protocol is implemented for the following type(s): DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, Jason.OrderedObject, List, Map, MapSet, Phoenix.LiveView.LiveStream, Postgrex.Stream, Range, Stream
    (elixir 1.16.0) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir 1.16.0) lib/enum.ex:178: Enumerable.count/1
    (elixir 1.16.0) lib/enum.ex:702: Enum.count/1
    iex:172: (file)
```

## <font color="red">値の取得／更新 … 動くがマップ時と挙動が異なる</font>

取得の時点でおかしいので、更新がそもそも出来ません

これはかなり厄介で、動かずエラーの方がマシなのに、中途半端に動いてしまうから、要注意です

```diff_elixir
iex> 1..10//2 |> Map.from_struct |> Enum.map(& &1)
[first: 1, last: 10, step: 2]

iex> 1..10//2 |> Enum.map(& &1)
[1, 3, 5, 7, 9]
```

## <font color="red">値の削除 … 動くがマップ時と挙動が異なる</font>

取得の時点でおかしいので、削除も出来ません

これもエラーが出ないけど挙動がデタラメなので、かなり厄介です

```diff_elixir
iex> 1..10//2 |> Map.from_struct |> Enum.drop(1)
[last: 10, step: 2]

iex> 1..10//2 |> Enum.drop(1)                   
[3, 5, 7, 9]
```