1
votes

I'd like to be able to exhaustively match on the types for implementations of a sealed trait, something like in the example below. Ideally I'd like to avoid using reflection (TypeTags) and implicit conversions if possible. Is there any way to exhaustively match on the type of a sealed trait?

object DataTypes {

  sealed trait StrFy {
    def stringify: String
  }

  final case class StrFyString(s: String) extends StrFy {
    def stringify = s
  }

  final case class StrFyInt(i: Int) extends StrFy {
    def stringify = i.toString
  }

  def stringifyThings[T <: StrFy](values: T*): String = {
    val label = T match {
      case StrFyString => "string"
      case StrFyInt => "integer"
      // cases that don't extend StrFy cause a compile error
    }

    "The " + label + " values are: " + values.map(_.stringify.fold("")(_+", "+_))
  }

  def printStringified(): Unit = {
    println(stringifyThings(StrFyString("foo"), StrFyString("bar"))) // should print: "the string values are: foo, bar"
    println(stringifyThings(StrFyInt(1), StrFyInt(2), StrFyInt(3))) // should print: "the integer values are: 1, 2, 3"
  }
}
1
What should you get for stringifyThings(StrFyString("s"), StrFyInt(9)) ?jwvh
jwvh: it should give an error (stringifyThings must take a homogenous list)marcprux

1 Answers

2
votes

There is no way to get a value given a type unless you use a typeclass which you ruled out by "no implicits". Therefore, you need to match against an instance.

object DataTypes extends App {

  sealed trait StrFy {
    def stringify: String
  }

  final case class StrFyString(s: String) extends StrFy {
    def stringify = s
  }

  final case class StrFyInt(i: Int) extends StrFy {
    def stringify = i.toString
  }

  def stringifyThings[T <: StrFy](values: T*): String = {
    def label(value: T) = value match {
      case _:StrFyString => "string"
      case _:StrFyInt => "integer"
      // cases that don't extend StrFy cause a compile error
    }
    // Will throw if values is empty
    "The " + label(values.head) + " values are: " + values.map(_.stringify).mkString(", ")
  }

  def printStringified(): Unit = {
    println(stringifyThings(StrFyString("foo"), StrFyString("bar"))) // should print: "the string values are: foo, bar"
    println(stringifyThings(StrFyInt(1), StrFyInt(2), StrFyInt(3))) // should print: "the integer values are: 1, 2, 3"
  }
  printStringified()
}

Implicits are not that scary though :) have a look:

object DataTypes extends App {

  sealed trait StrFy[T] {
    def stringify(value: T): String
    def label: String
  }

  implicit object StrFyString extends StrFy[String] {
    override def stringify(value: String): String = value
    override def label: String = "string"
  }
  implicit object StrFyInt extends StrFy[Int] {
    override def stringify(value: Int): String = value.toString
    override def label: String = "integer"
  }

  def stringifyThings[T: StrFy](values: T*): String = {
    val strFy = implicitly[StrFy[T]]
    // Safe even on empty values
    "The " + strFy.label + " values are: " + values.map(strFy.stringify).mkString(", ")
  }

  def printStringified(): Unit = {
    println(stringifyThings("foo", "bar")) // should print: "the string values are: foo, bar"
    println(stringifyThings(1, 2, 3)) // should print: "the integer values are: 1, 2, 3"
  }
  printStringified()
}

The compiler does the "pattern matching" for you, providing the correct instance given the input type. You see that typeclasses allow you to get a value, here label, given only a type. This is a quite fundamental concept that makes typeclass-like polymorphism stronger than the subtyping one :)