borer icon indicating copy to clipboard operation
borer copied to clipboard

Suggestion: Codec for unions to help Akka serialization

Open mushtaq opened this issue 5 years ago • 6 comments

Akka has a pattern for specifying serializers: first specify the serializer in akka.actor.serializers setting and then bind it with the types in akka.actor.serialization-bindings.

To avoid doing this for each top-level ADT, we mark every top-level ADT with a marker trait. This allows us to write just one serializer per application.

I am wondering if borer can help simplify writing this serializer by providing a utility to write codec for this Marker trait (which can not be a sealed). One way will be by providing forUnion helpers like below.


//Marker.scala
trait Marker
object Marker {
 implicit val encoder: Encoder[Marker] = Encoder.forUnion2[Marker, Adt1, Adt2]
 implicit val encoder: Decoder[Marker] = Decoder.forUnion2[Marker, Adt1, Adt2]
}

//Adt1.scala
sealed trait Adt1 extends Marker
object Adt1 {
  implicit val codec: Codec[Adt1] = ???
}

//Adt1.scala
sealed trait Adt1 extends Marker
object Adt2 {
  implicit val codec: Codec[Adt2] = ???
}

// my naive impl, will be good to have them for all arities. 
// Ideally, if one could just do `Codec.forUnion2[Marker, Adt1, Adt2]` to get both, that will be great.

def Encoder.forUnion2[B, T1 <: B: Encoder: ClassTag, T2 <: B: Encoder: ClassTag]: Encoder[B] = Encoder { (w, value) =>
  value match {
    case x: T1 => w.write(Map(nameOf[T1] -> x))
    case x: T2 => w.write(Map(nameOf[T2] -> x))
  }
}

def Decoder.forUnion2[B, T1 <: B: Decoder: ClassTag, T2 <: B: Decoder: ClassTag]: Decoder[B] = Decoder { r =>
  val name = r.readString()
  if (name == nameOf[T1]) r.read[T1]() else r.read[T2]()
}

def nameOf[T: ClassTag]: String = scala.reflect.classTag[T].runtimeClass.getSimpleName

mushtaq avatar Jan 27 '20 09:01 mushtaq

I see what you mean. Since the logic of the code generation for these forUnionX calls appears to be not very complex: Couldn't you simply write a small macro that generates the encoders, decoders and codecs?

Also, note that you can replace

w.write(Map(nameOf[T1] -> x))

with

w.writeMapOpen(1).writeString(nameOf[T1]).write(x).writeMapClose()

and avoid the intermediate Map and Iterator creation.

sirthias avatar Jan 27 '20 11:01 sirthias

Given that macros will need rework after Scala-3, our team has decided to not implement any in our projects. (But library macros are kosher :-)

I thought this will be more of sbt-boilerplate kind work than a macro, no?

For now, we have take a simpler approach. Sharing an example of that pattern for those who may want to use borer with Akka:

If we see perf issues, we may use reflection to replace List to a Map as done on a branch version of CborAkkaSerializer

So, you can close the issue if adding a sbt-boilerplate based impls for all arities does not feel appropriate to be part of the borer.

mushtaq avatar Feb 03 '20 06:02 mushtaq

Very interesting! Thank you, @mushtaq, for the pointers to your solution! Maybe your setup even warrants a blog post of some kind, to show, how the akka serializer infrastructure can be integrated with other libraries like borer. I'm sure that your approach is interesting to other people as well!

Since the problem appears to be a more general one that isn't quite specific to borer, I'm indeed inclined to not generate more boilerplate on borer's side at this point.

However, I'm gonna keep this issue open for some time to maybe attract more feedback/comments from other users.

sirthias avatar Feb 03 '20 11:02 sirthias

You are right about the blogpost, thanks. Our team will soon write it up.

mushtaq avatar Feb 03 '20 14:02 mushtaq

@sirthias

Based on the Akka Serializer pattern described in this issue, we have written a blog post - How to write your own Borer based Akka Serializer !!!. It covers our journey of evolving the Akka serializer pattern using Borer.

skvithalani avatar Feb 25 '20 15:02 skvithalani

Thank you for the pointer, @skvithalani!

sirthias avatar Feb 25 '20 15:02 sirthias