Greg
Greg

Reputation: 11542

In Scala, how can I avoid casting a function parameter?

I want to have various "flavors" of a component, each that handles a different "wire" format (e.g. String, Byte array, etc.). Example below. The innards of the read() function aren't important.

Note that on use I need to cast parameter "Heavy" to thing.WIRE to work. Since this is my top-level API I don't want the users to have to cast. They've chosen the flavor when they call FantasticThing.apply (or accept the default). After that I'd rather a cast not be needed.

How can I avoid the cast and have Scala realize that read() argument is a String based on StringFlavor being chosen?

trait Flavor {
  type WIRE
  def read[T](wire: WIRE)(implicit tt: TypeTag[T]): T
}

trait Maker {
  def make(): Flavor
}

object StringFlavor extends Maker {
  def make(): Flavor { type WIRE = String } = StringFlavor()
}

case class StringFlavor() extends Flavor {
  type WIRE = String
  def read[T](wire: String)(implicit tt: TypeTag[T]): T = {
    println(tt.tpe)
    if(tt.tpe =:= typeOf[Int]) {
      5.asInstanceOf[T]
    } else
      throw new Exception("Boom")
  }
}

object FantasticThing {
  def apply[WIRE](maker: Maker = StringFlavor): Flavor = maker.make()
}

object RunMe extends App {
  val thing: Flavor = FantasticThing(StringMaker)
  println(thing.read[Int]("Heavy".asInstanceOf[thing.WIRE])) // <-- How can I avoid this cast?
}

If I provide a bunch of Flavors then users should be able to do something like:

val foo = FantasticThing(ByteArrayFlavor)

Upvotes: 0

Views: 250

Answers (2)

gogstad
gogstad

Reputation: 3739

You can make WIRE a type parameter and propagate it through a type member or your Maker type. I.e:

import scala.reflect.runtime.universe._

trait Flavor[WIRE] {
  def read[T](wire: WIRE)(implicit tt: TypeTag[T]): T
}

trait Maker {
  type O
  def make(): Flavor[O]
}

object StringMaker extends Maker {
  type O = String
  def make(): Flavor[O] = StringFlavor()
}

case class StringFlavor() extends Flavor[String] {
  def read[T](wire: String)(implicit tt: TypeTag[T]): T = {
    if(tt.tpe =:= typeOf[Int]) {
      5.asInstanceOf[T]
    } else
      throw new Exception("Boom")
  }
}

object FantasticThing {
  def apply(): Flavor[String] = StringMaker.make()
  def apply(maker: Maker): Flavor[maker.O]  = maker.make() // Path dependent type.
}

object RunMe extends App {
  val thing: Flavor[String] = FantasticThing(StringMaker)
  thing.read[Int]("Heavy") // res0: Int = 5
}

Edit: Added no-arg apply() to this anwser. If a default value for maker is used (e.g. StringMaker) you get a compile error because argument "Heavy" is now supposed to be type Maker.O. Adding the no-arg apply solves this problem while providing the same experience to the caller.

Upvotes: 1

I took the liberty to modify your code with the intention to show how (what I understand of) your problem can be solved using typeclasses and type parameters, instead of type members.

import scala.reflect.runtime.universe.{TypeTag, typeOf}

implicit class Json(val underlying: String) extends AnyVal
implicit class Csv(val underlying: String) extends AnyVal

trait Flavor[W] {
  def read[T](wire: W)(implicit tt: TypeTag[T]): T
}

trait Maker[W] {
  def make(): Flavor[W]
}

object Maker {
  implicit val StringFlavorMaker: Maker[String] = new Maker[String] {
    override def make(): Flavor[String] = StringFlavor
  }

  implicit val JsonFlavorMaker: Maker[Json] = new Maker[Json] {
    override def make(): Flavor[Json] = JsonFlavor
  }

  implicit val CsvFlavorMaker: Maker[Csv] = new Maker[Csv] {
    override def make(): Flavor[Csv] = CsvFlavor
  }
}

case object StringFlavor extends Flavor[String] {
  override final def read[T](wire: String)(implicit tt: TypeTag[T]): T = {
    if(tt.tpe =:= typeOf[Int])
      0.asInstanceOf[T]
    else
      throw new Exception("Boom 1")
  }
}

case object JsonFlavor extends Flavor[Json] {
  override final def read[T](wire: Json)(implicit tt: TypeTag[T]): T = {
    if(tt.tpe =:= typeOf[Int])
      3.asInstanceOf[T]
    else
      throw new Exception("Boom 2")
  }
}

case object CsvFlavor extends Flavor[Csv] {
  override final def read[T](wire: Csv)(implicit tt: TypeTag[T]): T = {
    if(tt.tpe =:= typeOf[Int])
      5.asInstanceOf[T]
    else
      throw new Exception("Boom 3")
  }
} 

object FantasticThing {
  def apply[W](implicit maker: Maker[W]): Flavor[W] = maker.make()
}

Then you can create and user any flavour (given there is an implicit maker in scope) this way.

val stringFlavor = FantasticThing[String]
// stringFlavor: Flavor[String] = StringFlavor

stringFlavor.read[Int]("Heavy")
// res0: Int = 0

val jsonFlavor = FantasticThing[Json]
// jsonFlavor: Flavor[Json] = JsonFlavor

jsonFlavor.read[Int]("{'heavy':'true'}")
// res1: Int = 3

val csvFlavor = FantasticThing[Csv]
// csvFlavor: Flavor[Csv] = CsvFlavor

csvFlavor.read[Int]("Hea,vy")
// res2: Int = 0

In general, is better to stay off of type members, since they are more complex and used for more advanced stuff like path dependent types.
Let me know in the comments if you have any doubt.


DISCLAIMER: I am bad with type members (still learning about them), that may motivate me to use different alternatives. - In any case, I hope you can apply something similar to your real problem..

Upvotes: 1

Related Questions