user451151
user451151

Reputation: 406

Implement a final def within some trait where type parameters may or may not be the same

Some setup before my question:

/* roughly equivalent to a union type */
sealed trait NewType

object NewType {
  final case class Boolean(record: Boolean) extends NewType
  final case class Long(record: Long) extends NewType
  final case class String(record: String) extends NewType
}


/* Try to convert a record of type T to a NewType */
sealed trait NewTypeConverter[T] { def apply(record: T): Option[NewType] }

object NewTypeConverter {
  trait BooleanConverter[T] extends NewTypeConverter[T] {
    override def apply(record: T): Option[NewType.Boolean]
  }

  trait LongConverter[T] extends NewTypeConverter[T] {
    override def apply(record: T): Option[NewType.Long]
  }

  trait StringConverter[T] extends NewTypeConverter[T] {
    override def apply(record: T): Option[NewType.String]
  }
}

I want to define a trait Data like the following:

trait Data[T] {
  def name: String
  def converter: NewTypeConverter[_]
  final def value(record: T): Option[NewType] = ??? // calls converter
}

How can I implement this final def value(record: T): Option[NewType]?

A few things to note:

Upvotes: 0

Views: 64

Answers (1)

stefanobaghino
stefanobaghino

Reputation: 12804

It looks like you are 90% through implementing the Type Class pattern, so I'll try to solve your issue by completing it. Here is a nice reading about it. In short, what you miss is a signature that states that, if one (and only one) implementation of the converter can be found in the implicit scope, use it to run the conversion (or anything else defined by the trait).

The signature is the following:

final def value(record: T)(implicit c: NewTypeConverter[T]): Option[NewType]

Given such a strict signature also makes the implementation quite straightforward:

final def value(record: T)(implicit c: NewTypeConverter[T]): Option[NewType] =
  c(record) // literally only applies `c`, the converter

Now, whenever you have your converter instance in the implicit scope, for example the following:

implicit val converter: NewTypeConverter[Boolean] =
  new StringConverter[Boolean] {
    override def apply(record: Boolean): Option[NewType.String] =
      if (record) Some(NewType.String("TRUE"))
      else Some(NewType.String("FALSE"))
 }

you can instance your trait (simplified in the example):

trait Data[T] {
  def name: String
  final def value(record: T)(implicit c: NewTypeConverter[T]): Option[NewType] =
    c(record)
}

final case class BooleanData(name: String) extends Data[Boolean]

val bool = BooleanData(name = "foo")

And use it:

println(bool.value(true)) // prints Some(String(TRUE))
println(bool.value(false)) // prints Some(String(FALSE))

If you try to invoke the value method from a place where you have no access to the implicit instance you'll get an error:

error: could not find implicit value for parameter converter: NewTypeConverter[Boolean]

Bonus

Providing evidence for a known capability of an object via implicits is so common that Scala has some syntactic sugar you can use if you need to provide such evidence (for example, you have a method that calls your value method) but you do not have to use it directly. It's expressed as follows, with the : right after the generic type:

def methodThatCallsValue[T: Data](convertee: T): Option[NewType] =
  data.value(convertee)

it's called context bound and is equivalent to the following (which was done explicitly in the example):

def methodThatCallsValue(convertee: T)(implicit $ev: Data[T]): Option[NewType] =
  data.value(convertee)

Upvotes: 1

Related Questions