ちょっと便利な感じで Elixir で(列挙型)定数を定義する

## 列挙型が存在しない

フォームにセレクトボックスを置きたいな〜と思ってる今日このごろ。

(長ったらしい前置きなんていらねーよ！って方は[最後](#こいつ動くぞ完成形)まで飛んでください)

長くオブジェクト指向だけをやってきた身としては、どうしても定数を列挙型で定義して使いたくなってしまう...が、Elixir にはいわゆる列挙型(他言語のenum的な)が**存在しない**(と思う)。

`Enum` というモジュールはあるが、これはコレクションを操作するためのモジュールであって、期待している列挙型とは異なる。

存在しないということは、そもそも「列挙型」という考え方自体が Elixir に合っていないのかもしれない。

オシマイ。

## とりあえず見つけた方法でやってみる

...とは言ったものの、使いたいものは使いたい。
(Elixir らしい考え方や書き方を誰か教えてください)

色々と調べてみると下記の記事に行き着いた。
https://www.okb-shelf.work/entry/2019/10/20/232608

なるほど、試してみる。

```elixir:const.ex
defmodule Const do
  defmacro const(name, value) do
    def unquote(name), do: unquote(value)
  end
end 
```
```elixir:weather.ex
defmodule Weather do
  import Const

  const sunny, 1
  const cloudy, 2
  const rainy, 3
end
```
```elixir

iex(1)> Weather.sunny
10
```

うまくいってそう。

よし、じゃあこれを HTML のセレクトボックスに渡して...わた...どうやるんだ？

## 自動でリスト化できない

イメージとしては

```elixir
iex(1)> Weather.consts
[
  sunny: 1,
  cloudy: 2,
  rainy: 3
]
```

のようなものが取得できるのが理想だけど、前述のやり方では取得できない。

```elixir:weather.ex
defmodule Weather do
  import Const

  @sunny 1
  @cloudy 2
  @rainy 3

  const sunny, @sunny
  const cloudy, @cloudy
  const rainy, @rainy
end
```

と定数を使ってみたものの「定数の一覧を取得」ってどうやるんだ？

## ドキュメント？なにそれおいしいの？

どうやら `Module.__info__(:attributes)` がそれっぽい。
https://hexdocs.pm/elixir/Module.html#c:__info__/1

しかし、前述の `@sunny` のような定義の仕方では取得できない。

```elixir
iex(1)> Weather.__info__(:attributes)
[vsn: [12345678901234567890123456789012345678]]
```

どうやら `register_attribute/3` で永続化(`persist: true`)しないといけないらしい。
https://hexdocs.pm/elixir/Module.html#register_attribute/3
`@sunny` のような定数はコンパイルまでしか有効ではないと...難しい。

ということで、`register_attribute/3` と `put_attribute/3` を駆使して、

```elixir:const.ex
defmodule Const do
  defmacro const(name, value) do
    Module.register_attribute(
      unquote(__CALLER__.module), unquote(name), persist: true)
    Module.put_attribute(
      unquote(__CALLER__.module), unquote(name), unquote(value))
  end
end 
```
```elixir:weather.ex
defmodule Weather do
  import Const

  const :sunny, 1
  const :cloudy, 2
  const :rainy, 3
end
```
```elixir
iex(1)> Weather.__info__(:attributes)
[
  sunny: 1,
  cloudy: 2,
  rainy: 3
]
iex(2)> Weather.sunny
** (UndefinedFunctionError) function Weather.sunny/0 is undefined or private
    Weather.sunny()
```

あぁ…そうね、`def unquote(name), do: unquote(value)` を消したもんね。

```elixir:const.ex
defmodule Const do
  defmacro const(name, value) do
    def unquote(name), do: unquote(value)
    Module.register_attribute(
      unquote(__CALLER__.module), unquote(name), persist: true)
    Module.put_attribute(
      unquote(__CALLER__.module), unquote(name), unquote(value))
  end
end
```
```elixir
== Compilation error in file weather.ex ==
** (CompileError) weather.ex:4: undefined function sunny/0
```

あぁ...そうね、`unquote(name)` は関数名だから関数の引数に使えないよね。

- `name` にアトム(`:sunny`)を入れると関数名として使えない
- `name` に関数名(`sunny`)を入れると引数として使えない

...詰んだ＼(^o^)／

## 関数定義ってタプル...なの？

色々と試行錯誤しているうちに、関数名を `unquote(name.to_string)` とやってみたところ

```elixir
== Compilation error in file weather.ex ==
** (ArgumentError) you attempted to apply :to_string on {:sunny, [line: 4], nil}.
```

というエラーが...ん？もしかして関数の定義ってタプルで保持されているのか...？

じゃあ、`elem/2` で関数名をアトムで取り出せるじゃん！

## こいつ...動くぞ！(完成形)

試行錯誤して行き着いたコードがこちら。

```elixir:const.ex
defmodule Const do
  defmacro const(name, value) do
    def unquote(name), do: unquote(value)
    Module.register_attribute(
      unquote(__CALLER__.module), unquote(elem(name, 0)), persist: true)
    Module.put_attribute(
      unquote(__CALLER__.module), unquote(elem(name, 0)), unquote(value))
  end

  defmacro __using__(_opts) do
    quote do
      def consts do
        unquote(__CALLER__.module).__info__(:attributes)
        |> Keyword.delete(:vsn)
        |> Enum.map(fn({k, v}) -> {k, hd(v)} end) # [sunny: [1],...] => [sunny: 1,...]
      end
    end
  end
end 
```
```elixir:weather.ex
defmodule Weather do
  import Const
  use Const

  const sunny, 1
  const cloudy, 2
  const rainy, 3
end
```
```elixir
iex(1)> Weather.sunny
1
iex(2)> Weather.consts
[
  sunny: 1,
  cloudy: 2,
  rainy: 3
]
```

これこれ！
~~なんかすごく邪道な感じもするけど、~~意図通りに動いていた！