Advent Calendar、今年は私が Elixir で実装していて好きなパターンマッチングを題材にします。
前置き
これから書く内容は他のプログラミング言語にもあるかもしれません。
その場合は Elixir でも使えるんだね~、と置き換えていただければと思います。
条件分岐が辛い・・・
そんな経験はありませんか?
分岐が多かったり、if
の if
の if
があったり。
私の経験では条件分岐だけで1,000行以上のコードがあり、それに更に条件を追加するなんてことがありました。
ネストが深かったり1つの if
の中が長かったりすると実装しにくいですよね。
Elixir でも if
や case
を使うので、当然そういうケースに遭遇することがありますが、パターンマッチングを利用すると 解決することがあります。
制御構造
ここでは制御構造を使ったコードを掲載します。
制御構造に関しては Elixir School を参照してください。
例: 報酬タイプごとに報酬の配布を実装
ログインボーナスやクエストクリア時にもらえる報酬の配布処理を例にとりたいと思います。
報酬には「お金」「キャラクター」「アイテム」「ガチャを回す時などで使う石」の4種類の報酬があるとします。
報酬の種類ごとに配布処理が異なると、単純に実装したら下記のようになるでしょう。
defmodule Reward do
def present(user_id, reward_id, reward_type, quantity) do
case reward_type do
:money ->
Money.present(user_id, quantity)
:character ->
Character.present(user_id, reward_id, quantity)
:item ->
Item.present(user_id, reward_id, quantity)
:stone ->
Stone.present(user_id, quantity)
_ ->
# それ以外はプレゼントボックスへ
present_to_present_box(user_id, reward_id, reward_type, quantity)
end
end
end
ちなみに Elixir に else if
はなく、条件が複数の場合は case
を使うのが一般的です。
仕様追加1
ユーザが所持できるお金には上限があることが分かりました。
「お金が所持上限を超えたらその分はプレゼントボックスに配って!」
OK!
defmodule Reward do
def present(user_id, reward_id, reward_type, quantity) do
case reward_type do
:money ->
if Money.is_limit_over(user_id, quantity) do
Money.present_to_present_box(user_id, quantity)
else
Money.present(user_id, quantity)
end
:character ->
Character.present(user_id, reward_id, quantity)
:item ->
Item.present(user_id, reward_id, quantity)
:stone ->
Stone.present(user_id, quantity)
_ ->
# それ以外はプレゼントボックスへ
present_to_present_box(user_id, reward_id, reward_type, quantity)
end
end
end
仕様追加2
アイテムには だいじなもの というものがあり、これはアイテム一覧とは別に配布する必要があるそうです。
defmodule Reward do
def present(user_id, reward_id, reward_type, quantity) do
case reward_type do
:money ->
if Money.is_limit_over(user_id, quantity) do
Money.present_to_present_box(user_id, quantity)
else
Money.present(user_id, quantity)
end
:character ->
Character.present(user_id, reward_id, quantity)
:item ->
item = Item.get(reward_id)
case item.type do
:consume_item ->
Item.present(user_id, reward_id, quantity)
:important_item ->
Item.present_important_item(user_id, reward_id, quantity)
end
:stone ->
Stone.present(user_id, quantity)
_ ->
# それ以外はプレゼントボックスへ
present_to_present_box(user_id, reward_id, reward_type, quantity)
end
end
end
なかなかいい感じになってきました。
仕様追加3
このまま条件を追加し続けると崩壊寸前の塔が建ち上がりそうです。
追加する前になんとかしようと思いました。
関数にしてみよう
条件分岐が入り組んできたら、関数に分けてみるとスッキリします。
幸いにも Elixir では同じ関数名をモジュール内で宣言することができます。
同じ関数名を宣言できる条件は
- 引数の数が異なる
- ガード条件が付いている
- パターンマッチングを使っている
のいずれかを満たしている必要があります。
今回は パターンマッチング を使ってみます。
報酬の種類ごとに Reward.present/4
を定義するのです。
defmodule Reward do
def present(user_id, reward_id, :money, quantity) do
if Money.is_limit_over(user_id, quantity) do
Money.present_to_present_box(user_id, quantity)
else
Money.present(user_id, quantity)
end
end
def present(user_id, reward_id, :character, quantity) do
Character.present(user_id, reward_id, quantity)
end
def present(user_id, reward_id, :item, quantity) do
item = Item.get(reward_id)
case item.type do
:consume_item ->
Item.present(user_id, reward_id, quantity)
:important_item ->
Item.present_important_item(user_id, reward_id, quantity)
end
end
def present(user_id, reward_id, :stone, quantity) do
Stone.present(user_id, quantity)
end
def present(user_id, reward_id, reward_type, quantity) do
present_to_present_box()
end
end
あの長い条件分岐を大切にしておく必要がなくなりました。
これならば新しい報酬の種類が追加されても Reward.present/4
関数を追加するだけで済みますね。
アイテムに新しい仕様が追加されても、アイテム配布用の関数だけいじればいいので他の関数に影響はありません。
注意
関数のパターンマッチングは適切な reward_type を探すのではなく、上から順番にパターンマッチングを行います。
ですので最初に def present(user_id, reward_id, reward_type, quantity)
を宣言するとすべての報酬がプレゼントボックスに送られてしまいます。
Map のパターンマッチング
アイテムの報酬配布はさらにこんな感じに書くことができます。
defmodule Reward do
def present(user_id, reward_id, :item, quantity) do
item = Item.get(reward_id)
Item.present(user_id, item, quantity)
end
end
defmodule ItemStruct do
defstruct name: "", type: :consume_item
end
defmodule Item do
def get(item_id) do
# マスタからアイテムの名前とアイテムタイプを取得する処理
{name, item_type} = ItemMaster.get(item_id)
%ItemStruct{name: name, type: item_type}
end
def present(user_id, item, quantity) do
case item.type do
:consume_item ->
present_item(user_id, item, quantity)
:important_item ->
present_important_item(user_id, item, quantity)
end
end
end
ん~、これだと今度は Item.present/3
を何とかしたくなりますね・・・
こうならどうでしょう。アイテムの type
でパターンマッチングしてみます。
defmodule Reward do
def present(user_id, reward_id, :item, quantity) do
item = Item.get(reward_id)
Item.present(user_id, item, item.type, quantity)
end
defmodule ItemStruct do
defstruct name: "", type: :consume_item
end
defmodule Item do
def get(item_id) do
# マスタからアイテムの名前とアイテムタイプを取得する処理
{name, item_type} = ItemMaster.get(item_id)
%ItemStruct{name: name, type: item_type}
end
def present(user_id, item, :consume_item, quantity) do
present_item()
end
def present(user_id, item, :important_item, quantity) do
present_important_item()
end
end
case が消えましたね。
ただ、item を引数に渡しているのにわざわざ item.type
を渡すのはナンセンスですね。
その場合はこんな風にします。
defmodule Reward do
def present(user_id, reward_id, :item, quantity) do
item = Item.get(reward_id)
Item.present(user_id, item, quantity)
end
end
defmodule ItemStruct do
defstruct name: "", type: :consume_item
end
defmodule Item do
def get(item_id) do
# マスタからアイテムの名前とアイテムタイプを取得する処理
{name, item_type} = ItemMaster.get(item_id)
%ItemStruct{name: name, type: item_type}
end
def present(user_id, %ItemStruct{type: :consume_item} = item, quantity) do
present(user_id, item, quantity)
end
def give(user_id, %ItemStruct{type: :important_item} = item, quantity) do
give_important_item(user_id, item, quantity)
end
end
引数が Map だと、このように Map の要素でパターンマッチングすることができます。
終わりに
取り上げた例は結構無理やり感がありますが、条件分岐が多かったり、ネストが深くなってきた場合この手段がかなり役に立ってます。
自己満足ではありますが、こういう実装ができた時は何とも言えない爽快感を味わっております。
まだ Elixir を触ったことのない方に是非この爽快感を味わっていただきたいと願いながら、私の記事を終わりにしたいと思います。