scalaalgebraic-data-types

Scala 3, ADTs, combining elements in a collection


I'm new to Scala and trying to use Scala 3 enums as ADTs, and I have a collection I'm trying to "reduce" down in the following way:

enum Stats:
    case MaximumHealth(val x: Float)
    case HealthRegen(val x: Float)

def combineStats: Array[Stats] =
    val startWith = Array(Stats.MaximumHealth(20), Stats.MaximumHealth(10), Stats.HealthRegen(5))

    // how can I iterate/recurse over 'startWith' and "combine" multiple stats 
    // (such as MaximumHealth) entries into one?
    // (ie, producing endWith below)?

    val endWith = Array(Stats.MaximumHealth(30), Stats.HealthRegen(5))
    endWith

How can I do this? GroupBy is often suggested but I don't know how to group by just the enum case without the values of the parameters making Stats.MaximumHealth(20) and Stats.MaximumHealth(10) different groups.


Solution

  • This seems like a case for groupMapReduce, but first, I would prepare some methods on your enum to make life easier:

    enum Stats:
      // override is needed because of def x below
      case MaximumHealth(override val x: Float)
      case HealthRegen(override val x: Float)
    
      // the common getter for x
      def x: Float
    
      // combinator function for reducing
      def +(that: Stats): Stats = this match
        case MaximumHealth(x) => MaximumHealth(x + that.x)
        case HealthRegen(x)   => HealthRegen(x + that.x)
    

    Now, we can do this:

    def combineStats: Array[Stats] =
      val startWith = Array(Stats.MaximumHealth(20), Stats.MaximumHealth(10), Stats.HealthRegen(5))
    
      val reduced: Map[Int, Stats] =
        startWith.groupMapReduce(_.ordinal)(identity)(_ + _)
    
      val endWith = reduced.values.toArray
      endWith
    

    Alternative solution

    In above solution, I tried to keep your data structur as unchanged as possible. If I were to face that task, I would probably change it like this:

    enum StatsType:
      case MaximumHealth, HealthRegen
    
    case class Stats(statsType: StatsType, x: Float)
    

    Then one could do it like this:

    def combineStats(stats: List[Stats]): List[Stats] =
      val reduced: Map[StatsType, Float] =
        stats.groupMapReduce(_.statsType)(_.x)(_ + _)
      reduced.map{case (statsType, x) => Stats(statsType, x)}.toList
    

    Additional hint

    Not directly related to your question, but: if you want to keep your data structure as an enum as in your question, I'd consider making the x a member of the enum itself (as a simpler solution for a "common getter"):

    enum Stats(val value: Float):
      case MaximumHealth(x: Float) extends Stats(x)
      case HealthRegen(x: Float) extends Stats(x)