やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Elixirで定数を定義する

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! <- 変数の値が変化している

pythonrubyのように気軽に変数への代入を行うことが出来る一方でjavascriptで言う所のconstたる定数の宣言を標準の組み込み構文では行うことが出来ない

ピン演算子を使用することで値の束縛、すなわち再代入不可の状態を作り出すことは可能だが、どうせなら、何かしらのファイルにまとめて定数を宣言しておきたいので、うーん..となる

そんな中、以前twitterにてyamazakiさんに定数の作り方を教えて頂いたことを思い出し、参照を頼りにElixirで定数を実装してみた

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さんに教えて頂いた参照を頼りに動作を確認した

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)

やはり、ガード節で定数に対して条件を当てることは難しそうなので、先ほどの記述が良いんじゃないかな〜