Elixirには標準にconstがない
※書き方が早く知りたい方はこの章は読み飛ばして頂いて構いません
Elixirはいわゆる関数型言語だが変数の再代入、正しくはパターンマッチを何度も行うことが出来る。またgo-langのような厳密な型の指定も必要ない
user_input = "hello world!" IO.puts(user_input) user_input = "good bye hello!" IO.puts(user_input) # 実行結果 # hello world! # good bye hello! <- 変数の値が変化している
pythonやrubyのように気軽に変数への代入を行うことが出来る一方でjavascriptで言う所のconst
たる定数の宣言を標準の組み込み構文では行うことが出来ない
ピン演算子を使用することで値の束縛、すなわち再代入不可の状態を作り出すことは可能だが、どうせなら、何かしらのファイルにまとめて定数を宣言しておきたいので、うーん..となる
そんな中、以前twitterにてyamazakiさんに定数の作り方を教えて頂いたことを思い出し、参照を頼りにElixirで定数を実装してみた
Elixirで定数作るべと思い、以前yamazakiさんから教えて頂いた方法を採用して快適。あとはガード節でwhen foo == Constants.hoge() みたいな事が出来れば完璧。ヘルパー関数用意すれば出来るけど..うーんって感じで迷走中https://t.co/FqjZjW1rTF
— OKB (@sing_mascle69) 2019年10月17日
Elixirでの定数の置き方
大きくやり方は3種類ある
それぞれにメリットがあると思われるので、読者さんの気に入ったものを使って頂ければと思う
基本的な設計方針は外部ファイルに定数用のモジュールもしくはマクロを定義しており、それを本モジュールから呼び出して使用するという形になる。言葉で説明したところで良く分からないので、さっそくコードを見てみてほしい
ベーシックな書き方
定数の値を返す、関数をそれぞれ実装するスタイル
defmodule Const do def fruits(), do: ["apple", "banana", "orange"] def price("apple"), do: 120 def price("banana"), do: 90 def price("orange"), do: 80 end
使い方
defmodule Main do def main() do Const.fruits() |> Enum.map(fn val -> Const.price(val) end) |> Enum.sum() end end Main.main() |> IO.puts() # 290
簡単に使えて良い。関数の戻り値として成立すればどんな形であっても問題ない
モジュール内変数を渡す書き方
先ほどとやっていることはあまり変わらない
モジュール内に変数が定義できるsyntaxを利用して、その値を関数を利用して返すというスタイルになる
この方法の利点としてはベースの値をモジュール内変数として用意して、別途、用途に応じて変化させた値を返すことが出来るという点にある
defmodule Const do @fruits ["apple", "banana", "orange"] @min_price 60 def fruits(), do: @fruits def winter_fruits(), do: @fruits ++ ["grape", "strawberry"] def summer_fruits(), do: @fruits ++ ["Watermelon", "pineapple"] def choice_one(), do: Enum.random(@fruits) def price("apple"), do: 120 def price("banana"), do: 90 def price("orange"), do: 80 def price(_another), do: @min_price + :random.uniform(300) end
黒魔術(ブラックマジック)
Elixirのマクロを使ってconst構文を用意するやり方。こちらもyamazakiさんに教えて頂いた参照を頼りに動作を確認した
— Susumu Yamazaki (ZACKY) (@zacky1972) 2019年10月18日
const 関数名 値
とすることで指定した値を返す関数を作成することが可能になる
defmodule Const do defmacro const(name, value) do quote do def unquote(name), do: unquote(value) end end end
で、こいつを外部のモジュールから呼び出す
defmodule Sample do import Const const const_value, 1 def foo(num), do: const_value() == num end Sample.foo(1) |> IO.puts() Sample.foo(100) |> IO.puts() # true # false
すごい。個人的にマクロに関してはあまり理解していない & メタプログラミングよく分かってないので、別の機会に改めて学習しようと思う
おまけ(定数を使ってガード節でパターンマッチできないのつらい)
Constにリスト定義して、代入された値がリストに存在していなければ、パターンマッチで同名の別関数を実行させるという処理を行いたいが、ガード節ではConst.lists()のような値を呼び出すことが出来ない
defmodule Const do def fruits(), do: ["apple", "banana", "orange"] end defmodule Main do def main(val) when val in Const.fruits() do IO.puts("nice fruits") end def main(_val), do: IO.puts("normal fruits") end Main.main("apple")
こんな感じでerrorが出てしまう
** (ArgumentError) invalid args for operator "in", it expects a compile-time proper list or compile-time range on the right side when used in guard expressions, got: Const.fruits()
多少不満ではあるが、helper関数を用意して以下のように実装すればこの問題をクリアすることが出来た。再帰関数を実装する時にこの記述を良く使う
defmodule Main do def main(val) do is_exist? = val in Const.fruits() _main(is_exist?, val) end defp _main(true, val), do: IO.puts("nice fruits: #{val}") defp _main(false, _val), do: IO.puts("normal fruits") end Main.main("apple") # nice fruits: apple
ガード節で使用することが可能な関数は決まっており、この部分の仕様は分かれば別の解が思いつくかもしれない
ということでElixirの公式Documentsのガード節に関数部分を流し読みしてみた
まずガード節に対する公式の見解は以下(翻訳はノリでやってます)
- 使える条件式は意図的に制限している。なぜなら、ガードの中で意図せぬことが起こらぬように
- 引数だけでは表現不可な複雑なパターンマッチを行うためにガード節を用意している
- 時にはコードを最適化するに十分に機能する
ふむふむ、どうやらcustomのガード節を作成することが可能なようだが、ガード節に元々使用できない条件は利用不可なので、やりたいことは出来ないだろう
defmodule MyInteger do defguard is_even(value) when rem(value,2) == 0 and is_integer(value) end defmodule Main do import MyInteger, only: [is_even: 1] def my_function(number) when is_even(number) do # do stuff IO.puts("nice") end end Main.my_function(4)
やはり、ガード節で定数に対して条件を当てることは難しそうなので、先ほどの記述が良いんじゃないかな〜