1

Say I have a method like this:

def getClassFromIterable(iterable: Iterable[Any]): Class[_] = {
  iterable.head.getClass
}

This will get the class of the top of the list, but will fail if the list is empty.
How can I get the class of a passed list that has zero or more elements?

Jared DuPont
  • 165
  • 2
  • 14
  • Why do you need this in the first place? Using **Any** is a code smell and requiring reflection to get the `class` of an element _(and confuse that with its type)_ is a sign of a design problem. – Luis Miguel Mejía Suárez Feb 17 '20 at 20:39
  • I am making a function to turn a Iterable of case classes (or any object) into a 2D string table of it's fields, a simple example would turn a list of Points into a 2 wide table with X and Y as the column headers – Jared DuPont Feb 17 '20 at 20:41
  • 1
    You do not need this method, just ask for a [**ClasTag**](https://www.scala-lang.org/api/current/scala/reflect/ClassTag.html) on your original method. Something like `def createTable[T](data: Iterable[T])(implicit ct: ClassTag[T])` you can call `runtimeclass` on it. – Luis Miguel Mejía Suárez Feb 17 '20 at 21:34
  • That appears to have solved my question, if you post it as an answer I will accept it – Jared DuPont Feb 17 '20 at 21:49

2 Answers2

2

Supplementing Luis' answer with a reflective solution, consider

import scala.reflect.runtime.universe._

def toTable[A <: Product](ps: List[A])(implicit ev: TypeTag[A]) = {
  val separator = "\t\t"
  ps match {
    case Nil =>
      val header = typeOf[A].members.collect { case m: MethodSymbol if m.isCaseAccessor => m.name }.toList
      header.mkString("", separator , "\n")

    case head :: _ =>
      val header = head.productElementNames.toList
      val rows = ps.map(_.productIterator.mkString(separator))
      header.mkString("", separator, "\n") + rows.mkString("\n")
  }
}

which outputs

case class Point(x: Double, y: Double)
println(toTable(List(Point(1,2), Point(3,4))))

x       y
1.0     2.0
3.0     4.0

Based on Get field names list from case class

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
1

For this kind of problems, please consider using a typeclass instead.

import scala.collection.immutable.ArraySeq

trait TableEncoder[T] {
  def header: ArraySeq[String]
  def asRow(t: T): ArraySeq[String]
}

object TableEncoder {
  def toTable[T](data: IterableOnce[T])
                (implicit encoder: TableEncoder[T]): ArraySeq[ArraySeq[String]] = {
    val builder = ArraySeq.newBuilder[ArraySeq[String]]

    builder.addOne(encoder.header)
    builder.addAll(data.iterator.map(encoder.asRow))

    builder.result()
  }
}

Which you can use like this:

final case class Point(x: Int, y: Int)
object Point {
  final implicit val PointTableEncoder: TableEncoder[Point] =
    new TableEncoder[Point] {
      override val header: ArraySeq[String] =
        ArraySeq("x", "y")

      override def asRow(point: Point): ArraySeq[String] =
        ArraySeq(
          point.x.toString,
          point.y.toString
        )
    }
}

TableEncoder.toTable(List(Point(1, 2), Point(3, 3))) 
// res: ArraySeq[ArraySeq[String]] = ArraySeq(
//   ArraySeq("x", "y"),
//   ArraySeq("1", "2"),
//   ArraySeq("3", "3")
// )
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • The point of the method is to generate a table without me having to manually determine the headers or to define a toRow function. I have everything working except for the case of the table being empty, hence the question – Jared DuPont Feb 17 '20 at 21:17
  • @JaredDuPont You may want to take a look at shapeless or magnolia to ease the process of deriving the **typeclass**, but believe me, this kind of boilerplate is worth it. It is safer since it will only compile if the value can be turned into a table.It is faster since it doesn't use reflection. It is also more flexible and secure in the sense that you define what you want to be exposed and in which order. – Luis Miguel Mejía Suárez Feb 17 '20 at 21:21