LoginSignup
5
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-06

列挙型が存在しない

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

(長ったらしい前置きなんていらねーよ!って方は最後まで飛んでください)

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

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

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

オシマイ。

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

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

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

なるほど、試してみる。

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

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

iex(1)> Weather.sunny
10

うまくいってそう。

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

自動でリスト化できない

イメージとしては

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

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

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 のような定義の仕方では取得できない。

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

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

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

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 
weather.ex
defmodule Weather do
  import Const

  const :sunny, 1
  const :cloudy, 2
  const :rainy, 3
end
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) を消したもんね。

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
== 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) とやってみたところ

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

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

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

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

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

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 
weather.ex
defmodule Weather do
  import Const
  use Const

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

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

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1