6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-08

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

過去の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.「いいね」よろしくお願いします

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

6
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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?