Search code examples
scalapattern-matchingcase-class

Scala: How to pattern-match the enclosing object of an inner case class?


I have an inner case class, specifically an event from this question, and want to match it, including the outer object:

class Player {
  var _life = 20
  def life = _life

  def gainLife(life: Int) = execute(GainLife(life))

  case class GainLife(life: Int) extends Event {
    def execute() = _life += life
  }
}

I can easily write an effect (a partial function) that replaces life events for a specific player:

//gain twice as much life
def effect(player: Player): ReplacementEffect = {
  case player.GainLife(x) => player.GainLife(x * 2)
}

however, I can't do the same for other players. The closest I have come is this:

//only you gain life
def effect2(player: Player): ReplacementEffect = {
  case evt: Player#GainLife => player.GainLife(evt.life)
}

But 1) this replaces even your own lifegain with a new lifegain, 2) I can't reference the player that originally gained life in the function and 3) I'm missing out on directly matching life this way.

This could be expressed using a path-independent type like

case Player.GainLife(_player, life) if _player != player => GainLife(player, life)

Ideally, I want something like

case _player.GainLife(life) if _player != player => player.GainLife(life)

Is this possible somehow, or can I work around this? Or do I have to resort to making GainLife nested?


Solution

  • The closest I have come is to define my own unapply method:

    class Player {
      self =>
    
      var _life = 20
      def life = _life
    
      def gainLife(life: Int) = execute(GainLife(life))
    
      case class GainLife(life: Int) extends Event {
        def player = self
    
        def execute() = _life += life
      }
    }
    
    object Player {
      object _GainLife {
        def unapply(event: Player#GainLife) =
          Some((event.player, event.life))
      }
    }
    

    Note that naming the Player._GainLife object Player.GainLife instead would cause a name conflict, that is the most important downside here. Therefore, I chose to make that type available from outside the Player namespace:

    val GainLife = Player._GainLife
    

    This allows to match using both player.GainLife.unapply and Player._GainLife.unapply in a concise way:

    //gain twice as much life
    def effect1(player: Player): ReplacementEffect = {
      case player.GainLife(life) => player.GainLife(life * 2)
    }
    
    //only you gain life
    def effect2(player: Player): ReplacementEffect = {
      case GainLife(_player, life) if _player != player => player.GainLife(life)
    }
    
    //all players gain twice as much life
    def effect3: ReplacementEffect = {
      case GainLife(player, life) => player.GainLife(life * 2)
    }
    

    The last two examples look a little asymmetric, but that can be fixed with an apply method if desired.