過去コラムZEN Style化①:再帰で書かれたkeyによるロジック切り替え(Strategy/StateパターンをElixirらしくスマートに)


fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます

過去のQiita Elixirコラムを、「Elixir ZEN Style」で書き換えると、どうなるかのミニコラムです

今回は、@darui_kara さんが書かれた下記コラムでやってみます

[Elixir]キーワードリストのkeyによって処理を変える一例
https://qiita.com/darui_kara/items/364b75f1cd8d28df6272

なお、「Elixir ZEN Style」については、下記コラムをご覧ください

Elixir Zen スタイルプログラミング講座
https://qiita.com/zacky1972/items/619f39cc77fbb52b1bbf

本コラムの検証環境

本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)

キーワードリストのkeyによってロジックを切り替える

キーワードリストのkey毎に、異なる処理を実行できるような切り替えを、登録済みkeyのリストと、関数のガードで実現しているのが、元コラムの内容です

モジュール内の「@変数」にkeyのリストを登録し、ガード「in」でリストとのマッチングを行います

defmodule ArgumentsSample do

  @spec sample(Keyword.t) :: any
  def sample(kw) do
    keys = Keyword.keys(kw)
    values = Keyword.values(kw)

    IO.puts "Start..."
    sample(keys, values)
  end

  @match1 [:hoge, :huge]
  @match2 [:foo, :bar]

  defp sample([key|keys_tail], [value|values_tail]) when key in @match1 do
    IO.puts "@match1 match"
    IO.puts "#{key} = #{value}"
    sample(keys_tail, values_tail)
  end

  defp sample([key|keys_tail], [value|values_tail]) when key in @match2 do
    IO.puts "@match2 match"
    IO.puts "#{key} = #{value}"
    sample(keys_tail, values_tail)
  end

  defp sample(key, value) do
    IO.puts "...End"
  end
end

実行結果は、以下の通りです

iex> ArgumentsSample.sample hoge: 1, foo: 2, bar: 3, hoge: 4, huge: 5
Start...
@match1 match
hoge = 1
@match2 match
foo = 2
@match2 match
bar = 3
@match1 match
hoge = 4
@match1 match
huge = 5
...End
:ok

元のコードが、再帰で実装されているところを、Enum.eachでElixir ZEN Style化します

defmodule ArgumentsSample do

  @spec sample( Keyword.t ) :: any
  def sample( kw ) when is_list( kw ) do
    IO.puts "Start..."
    kw |> Enum.each( & sample( &1 ) )
    IO.puts "...End"
  end

  @match1 [ :hoge, :huge ]
  @match2 [ :foo,  :bar  ]

  def sample( { key, value } ) when key in @match1 do
    IO.puts "@match1 match"
    IO.puts "#{ key } = #{ value }"
  end
  def sample( { key, value } ) when key in @match2 do
    IO.puts "@match2 match"
    IO.puts "#{ key } = #{ value }"
  end
end

実行結果は変わらず、同じ結果が出力されます

書き換えのポイント

  • 再帰によるフロー制御を無くすことで、各sampleからフロー制御を除去でき、リスト分割も除去できました
  • この結果、keysとvaluesを事前に取り出す中間処理が不要になりました
  • 最終メッセージ出力を、再帰の終端(≒空リスト)として、最後のsampleでやっていたが、メイン側に移せました

効果

  • コード行数が30%削減できました
  • 制御フローと処理の分割で見通しが良くなりました

このテクニックの使いどころ

いわゆる「Strategyパターン」や「Stateパターン」のような、入力や状態によって、ハンドラをスイッチするようなものであれば、このテクニックによって、非常に保守性が高く、見通しの良いコードとなります(いちいちクラスを作るウザさもElixirなので当然ありません)

キーワードリストに入力や状態を並べ、ハンドラを呼ぶkey毎にグルーピングを行えばOKです

なお、各リストにkeyが単数しか無い場合は、下記のように、関数パターンマッチとして直接書けば良いため、inによるガードも、「@変数」の登録も、不要になります


  def sample( { :hoge, value } ) do
    IO.puts "@match1 match"
    IO.puts "#{ key } = #{ value }"
  end
  def sample( { :foo, value } ) do
    IO.puts "@match2 match"
    IO.puts "#{ key } = #{ value }"
  end

ちなみに…

ガードの「in」の指定が、「@変数」で無いと、エラーになります

これは不思議な仕様のように見えますが、ガードに書ける構文には、割と制約がある(動的な関数や自己定義関数はまず使えない)ので、致し方ありません

恐らく、「@変数」は、コンパイル時に定数であるため、この制約に引っかからないのでしょう

p.s.「いいね」よろしくお願いします

ページ左上の のクリックを、どうぞよろしくお願いします
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!