列挙型が存在しない
フォームにセレクトボックスを置きたいな〜と思ってる今日このごろ。
(長ったらしい前置きなんていらねーよ!って方は最後まで飛んでください)
長くオブジェクト指向だけをやってきた身としては、どうしても定数を列挙型で定義して使いたくなってしまう...が、Elixir にはいわゆる列挙型(他言語のenum的な)が存在しない(と思う)。
Enum
というモジュールはあるが、これはコレクションを操作するためのモジュールであって、期待している列挙型とは異なる。
存在しないということは、そもそも「列挙型」という考え方自体が Elixir に合っていないのかもしれない。
オシマイ。
とりあえず見つけた方法でやってみる
...とは言ったものの、使いたいものは使いたい。
(Elixir らしい考え方や書き方を誰か教えてください)
色々と調べてみると下記の記事に行き着いた。
https://www.okb-shelf.work/entry/2019/10/20/232608
なるほど、試してみる。
defmodule Const do
defmacro const(name, value) do
def unquote(name), do: unquote(value)
end
end
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
]
のようなものが取得できるのが理想だけど、前述のやり方では取得できない。
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/3
と put_attribute/3
を駆使して、
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
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)
を消したもんね。
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
で関数名をアトムで取り出せるじゃん!
こいつ...動くぞ!(完成形)
試行錯誤して行き着いたコードがこちら。
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
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
]
これこれ!
~~なんかすごく邪道な感じもするけど、~~意図通りに動いていた!