LoginSignup
6
0

More than 3 years have passed since last update.

Elixir のパターンマッチング ~長い条件分岐よ、さようなら~

Last updated at Posted at 2019-12-21

Advent Calendar、今年は私が Elixir で実装していて好きなパターンマッチングを題材にします。

前置き

これから書く内容は他のプログラミング言語にもあるかもしれません。
その場合は Elixir でも使えるんだね~、と置き換えていただければと思います。

条件分岐が辛い・・・

そんな経験はありませんか?
分岐が多かったり、ififif があったり。
私の経験では条件分岐だけで1,000行以上のコードがあり、それに更に条件を追加するなんてことがありました。
ネストが深かったり1つの if の中が長かったりすると実装しにくいですよね。
Elixir でも ifcase を使うので、当然そういうケースに遭遇することがありますが、パターンマッチングを利用すると 解決することがあります。

制御構造

ここでは制御構造を使ったコードを掲載します。
制御構造に関しては 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 を触ったことのない方に是非この爽快感を味わっていただきたいと願いながら、私の記事を終わりにしたいと思います。

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