Reduce function on custom data structure using Cats Semigroup This is for purely pedagogical purposes only. So, within the contrived data structure and its constraints, I'm interested in knowing how might one improve that reduce function and whether I've properly used the Semigroup typeclass correctly here.

object ComplexData {
  import cats.Semigroup
  import cats.instances.all._

  case class Element(name: String, value: Int, els: List[Element] = Nil) {

  def reduce[A : Semigroup](z: A)(f: Element =&gt; A): A = {
      Semigroup[A].combine(f(this),
        els match {
          case Nil =&gt; z
          case _ =&gt; els.map(_.reduce(z)(f)).reduce((a, b) =&gt; Semigroup[A].combine(a, b))
        })
      }
    }

    val e1 = Element("Zero", 0,
      List(
        Element("One", 1,
          List(Element("Two", 2,
            List(
             Element("Three", 3),
             Element("Four", 4))))),
        Element("Five", 5,
          List(
           Element("Six", 6),
           Element("Seven", 7),
           Element("Eight", 8,
             List(
               Element("Nine", 9),
               Element("Ten", 10)))))))

   e1.reduce(0)(_.value) 
   //res0: Int = 55

   e1.reduce("")(_.name + " ") 
   //res1: String = Zero One Two Three...

   e1.reduce(0)(_.els.length) 
   //res2: Int = 10

   e1.reduce("")(e =&gt; e.name + " -&gt; " + e.value + ", ")
   //res3: String = One -&gt; 1, Two -&gt; 2, Three -&gt; 3, ...
 }


Specifically: 


While it works, not excited by the use of view bounds given that they are long since 
deprecated (attempting to use A &lt;: Semigroup[A]in the function signature did not compile), do I really need an implicit definition here of the semigroup if I wanted to go this way?
That pattern match seems accidentally complex, even given my constraints there's probably a more elegant or at least more straightforward way to do that, yes?
If I used aMonoid[A]instead of a semigroup I could get rid of thezparameter and provide aZero[A]orEmpty[A], I think - is that the preferred way to go?
Yes, using a Monoid can simplify what you want:

def foldMonoidal[A : Monoid](f: Element =&gt; A): A =
      Monoid.combine(f(this), Monoid.combineAll(els.map(f)))
...

e1.foldMonoidal(_.value)


One issue with that though is that you lose the information of the nested structure: You may as well have just a list of (name, value).

Any nested data structure can have a fold operation defined for it, in a way that keeps knowledge of its structure. In your case, this would be:

def fold[A](f: (Element, List[A]) =&gt; A): A =
  f(this, els.map(_.fold(f)))
...
e1.fold[Int]((el, list) =&gt; el.value + list.sum)


You can see this allows more freedom, e.g. you may want to sum the value with the average of the nested elements, which you couldn't do with the monoidal solution above.

e1.fold[Double]((el, list) =&gt; el.value + list.sum / list.size)


Or for pretty-printing:

e1.fold[String]((el, strEls) =&gt; s"(${el.name} -&gt; ${el.value}, ${strEls.mkString("[", ",", "]")}")
// (Zero -&gt; 0, [(One -&gt; 1, [(Two -&gt; 2, [(Three -&gt; 3, [],(Four -&gt; 4, []]],(Five -&gt; 5, [(Six -&gt; 6, [],(Seven -&gt; 7, [],(Eight -&gt; 8, [(Nine -&gt; 9, [],(Ten -&gt; 10, []]]]