Reputation: 1099
Okay, maybe I am tackling the whole thing the wrong way, but I need help, I can't find a solution.
I am working on wrapping up a Redis client implementation for my own use in Scala.
So I want to have a class called RedisListClient[T]
which will internally handle all things, and I just have a List[T]
to work with.
Now I have a factory creating such a client in the following way:
def createRedisListClient[T <: ByteStringFormattable](name: String): RedisListClient[T] = {
new RedisListClient[T](name)
}
Where the generic type has to include the trait ByteStringFormattable
. That's needed cause the redis client has to internally serialize the objects to a ByteString, and has to be able to do that backwards.
The needed trait is as easy as
trait ByteStringFormattable {
type T
val formatter: ByteStringFormatter[T]
}
So I got it all working for a custom class. Let's say a ScoreModel, where scores can be saved.
class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable {
override type T = ScoreModel
override val formatter: ByteStringFormatter[T] = new ByteStringFormatter[T] {
override def serialize(data: T): ByteString = ByteString(s"$userId#$score#$scoreText")
override def deserialize(bs: ByteString): T = {
val split = bs.utf8String.split("#", 2)
new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
}
}
}
The real important question for me is now, how will I be able to use createRedisListClient[String]("myName")
?
I mean String
does not implement the trait ByteStringFormattable
.
I tried using implicit conversions like this:
implicit def fromString(s: String): SerializableString = new SerializableString(s)
implicit def toString(sws: SerializableString): String = sws.get
implicit class SerializableString(string: String) extends ByteStringFormattable {
override type T = String
val get = string
override val formatter: ByteStringFormatter[T] = new ByteStringFormatter[String] {
override def serialize(data: String): ByteString = ByteString(data.getBytes("UTF8"))
override def deserialize(bs: ByteString): String = bs.utf8String
}
}
But that doesn't help. The Scala compiler sees the type String for that function call, checks if it uses the trait, and fails.
So, is there any possibility that I can use the function call with basic types like String
, Int
and Co with some implicit stuff, or do I really have to build Wrapper classes like the SerializableString
in the example above?
Upvotes: 3
Views: 259
Reputation: 23578
The shortest change from what you have to working code is to use a view bound instead of the bound you have now. A view bound would assert that there's some sort of implicit conversion from T
to ByteStringFormattable
, which covers the case where T
is a subclass but also covers the case you have there with String
.
However, before we do that I think a bit of re-arrangement is in order. Specifically, I think that your definition of ByteStringFormattable
is a problem, because there's no connection between the type being converted and the ByteStringFormattable
. As is, right now you could write this and it would compile even though it's pretty wrong:
class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable {
override type T = String
override val formatter: ByteStringFormatter[T] = new ByteStringFormatter[T] {
override def serialize(data: String): ByteString = ByteString(data.getBytes("UTF8"))
override def deserialize(bs: ByteString): String = bs.utf8String
}
}
That is, right now you don't have a type guarantee that the ByteStringFormatter
returned will be a formatter for the correct class.
Therefore, I would redefine ByteStringFormattable
as:
trait ByteStringFormattable[T] {
val formatter: ByteStringFormatter[T]
}
Now then, here's something I wrote up that compiles doing what you want with a view bound. But keep reading, since view bounds are deprecated in Scala 2.11 and so I show the better, non-deprecated way later (implicit parameters).
Note that I had to tweak your calls to methods on ByteString to get it to compile here because I don't have redis's ByteString sitting around, and had to use the one from com.google.protobuf
.
class RedisListClient[T <% ByteStringFormattable[T]](val name: String) {
}
trait ByteStringFormatter[T] {
def serialize(data: T): ByteString
def deserialize(bs: ByteString): T
}
trait ByteStringFormattable[T] {
val formatter: ByteStringFormatter[T]
}
class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable[ScoreModel] {
override val formatter: ByteStringFormatter[ScoreModel] = new ByteStringFormatter[ScoreModel] {
override def serialize(data: ScoreModel): ByteString = ByteString.copyFromUtf8(s"$userId#$score#$scoreText")
override def deserialize(bs: ByteString): ScoreModel = {
val split = bs.toStringUtf8.split("#", 2)
new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
}
}
}
object RedisListClient {
implicit class SerializableString(string: String) extends ByteStringFormattable[String] {
val get = string
override val formatter: ByteStringFormatter[String] = new ByteStringFormatter[String] {
override def serialize(data: String): ByteString = ByteString.copyFrom(data.getBytes("UTF8"))
override def deserialize(bs: ByteString): String = bs.toStringUtf8
}
}
def createRedisListClient[T <% ByteStringFormattable[T]](name: String): RedisListClient[T] = {
new RedisListClient[T](name)
}
def testIt() = {
val x = createRedisListClient[ScoreModel]("ClientSM")
val y = createRedisListClient[String]("Wat")
}
}
That compiles, but as I said it uses deprecated view bounds, which are in modern scala replaced by implicit parameters. Here's a way to do what you wanted with implicit parameters:
class RedisListClient[T](val name: String, conv: T => ByteStringFormattable[T]) {
}
trait ByteStringFormatter[T] {
def serialize(data: T): ByteString
def deserialize(bs: ByteString): T
}
trait ByteStringFormattable[T] {
val formatter: ByteStringFormatter[T]
}
class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) extends ByteStringFormattable[ScoreModel] {
override val formatter: ByteStringFormatter[ScoreModel] = new ByteStringFormatter[ScoreModel] {
override def serialize(data: ScoreModel): ByteString = ByteString.copyFromUtf8(s"$userId#$score#$scoreText")
override def deserialize(bs: ByteString): ScoreModel = {
val split = bs.toStringUtf8.split("#", 2)
new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
}
}
}
object RedisListClient {
implicit class SerializableString(string: String) extends ByteStringFormattable[String] {
val get = string
override val formatter: ByteStringFormatter[String] = new ByteStringFormatter[String] {
override def serialize(data: String): ByteString = ByteString.copyFrom(data.getBytes("UTF8"))
override def deserialize(bs: ByteString): String = bs.toStringUtf8
}
}
def createRedisListClient[T](name: String)(implicit mkFormatter: T => ByteStringFormattable[T]): RedisListClient[T] = {
new RedisListClient[T](name, mkFormatter)
}
def testIt() = {
val x = createRedisListClient[ScoreModel]("ClientSM")
val y = createRedisListClient[String]("Wat")
}
}
And that's all well and good, but if you're going to be using implicit parameters already, what you probably want is implicit ByteStringFormatter
objects, rather than having to call formatter
on each element. So let's rework it one more time, but with implicit formatter objects, and no formattable trait at all:
class RedisListClient[T](val name: String, formatter: ByteStringFormatter[T]) {
}
trait ByteStringFormatter[T] {
def serialize(data: T): ByteString
def deserialize(bs: ByteString): T
}
class ScoreModel(userId: String, score: Long, scoreText: Option[String] = None) {
def toBs: ByteString = ByteString.copyFromUtf8(s"$userId#$score#$scoreText")
}
object RedisListClientImplicits {
implicit val formatterSM: ByteStringFormatter[ScoreModel] = new ByteStringFormatter[ScoreModel] {
override def serialize(data: ScoreModel): ByteString = data.toBs
override def deserialize(bs: ByteString): ScoreModel = {
val split = bs.toStringUtf8.split("#", 2)
new ScoreModel(split(0), split(1).toLong, split.lift(2).map(s => Some(s)).getOrElse(None))
}
}
implicit val formatterString: ByteStringFormatter[String] = new ByteStringFormatter[String] {
override def serialize(data: String): ByteString = ByteString.copyFrom(data.getBytes("UTF8"))
override def deserialize(bs: ByteString): String = bs.toStringUtf8
}
}
object RedisListClient {
def createRedisListClient[T](name: String)(implicit f: ByteStringFormatter[T]): RedisListClient[T] = {
new RedisListClient[T](name, f)
}
}
object RedisListClientTest {
import RedisListClient._
import RedisListClientImplicits._
def testIt() = {
val x = createRedisListClient[ScoreModel]("ClientSM")
val y = createRedisListClient[String]("Wat")
}
}
Upvotes: 1
Reputation: 2337
Do you really want to call your method like this:
createRedisListClient[String]("myName")
I.e. is it important to you that the type parameter is String? As mentioned in the other answer this can not work due to your type constraint, you would have to change that to a context bound.
But is that really what you want, or do you actually not care what the type parameter will be and just want to convert String to something suitable implicitly?
If you called it like this your implicit conversions can be applied:
createRedisListClient("myName")
You have to remove your implicit conversion methods though. These methods are generated for an implicit class. (Actually, generating these methods is the only thing declaring a class implicit does).
Upvotes: 0
Reputation: 9698
I don't have time to write the whole code for you since you have all these Redis dependencies so it's tedious for me to reproduce, but let me explain generally.
Your method
def createRedisListClient[T <: ByteStringFormattable](name: String)
can only accept something that extends ByteStringFormattable
. If you want it to be able to accept type T for which an implicit conversion T -> ByteStringFormattable is available in scope, you must say so:
def createRedisListClient[T](name: String)(implicit conv: T => ByteStringFormattable)
You can also do it with syntax sugar such as view bounds and context bounds (view bounds have been deprecated so I would avoid them).
On a more general note, standard solution to your problem are type classes. Let me also point you at my blog post on this topic. I wrote it because I felt that we needed an article which glues together all this stuff, from the problem you are having to implicit conversions, view/context bounds and type classes. If you have 15-20 minutes to spare, I think it would be helpful to you.
Upvotes: 2