Reputation: 406
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:
apply
method of converter
must be the same return type as value
. So, if you happen to have a BooleanConverter
, then value
must return an Option[NewValue.Boolean]
.T
to the Data
trait does not have to be the same as the input type _
to the converter
. If they happen to be the same type, then the implementation could just be final def value(record: T): Option[NewType] = converter(record)
. The trickier case is in when the input types differ. Let's say the input type to Data
was a String
, but the input type to converter
was a Long
. How would that be handled?Upvotes: 0
Views: 64
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]
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