Injection: Creating Custom Encoders
Injection lets us define encoders for types that do not have one by injecting A
into an encodable type B
.
This is the definition of the injection typeclass:
trait Injection[A, B] extends Serializable {
def apply(a: A): B
def invert(b: B): A
}
Example
Let's define a simple case class:
case class Person(age: Int, birthday: java.util.Calendar)
val people = Seq(Person(42, new java.util.GregorianCalendar()))
// people: Seq[Person] = List(
// Person(
// 42,
// java.util.GregorianCalendar[time=1735181841807,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Etc/UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2024,MONTH=11,WEEK_OF_YEAR=52,WEEK_OF_MONTH=4,DAY_OF_MONTH=26,DAY_OF_YEAR=361,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=4,AM_PM=0,HOUR=2,HOUR_OF_DAY=2,MINUTE=57,SECOND=21,MILLISECOND=807,ZONE_OFFSET=0,DST_OFFSET=0]
// )
// )
And an instance of a TypedDataset
:
val personDS = TypedDataset.create(people)
// error: could not find implicit value for parameter encoder: frameless.TypedEncoder[repl.MdocSession.MdocApp0.Person]
// val personDS = TypedDataset.create(people)
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
Looks like we can't, a TypedEncoder
instance of Person
is not available, or more precisely for java.util.Calendar
.
But we can define a injection from java.util.Calendar
to an encodable type, like Long
:
import java.util.Calendar
import frameless._
implicit val calendarToLongInjection = new Injection[Calendar, Long] {
def apply(d: Calendar): Long = d.getTime.getTime
def invert(l: Long): Calendar = {
val cal = new java.util.GregorianCalendar()
cal.setTime(new java.util.Date(l))
cal
}
}
// calendarToLongInjection: AnyRef with Injection[Calendar, Long] = repl.MdocSession$MdocApp0$$anon$1@79981a97
We can be less verbose using the Injection.apply
function:
import frameless._
import java.util.Calendar
implicit val calendarToLongInjection = Injection[Calendar, Long](
(_: Calendar).getTime.getTime,
{ (l: Long) =>
val cal = new java.util.GregorianCalendar()
cal.setTime(new java.util.Date(l))
cal
})
// calendarToLongInjection: Injection[Calendar, Long] = frameless.Injection$$anon$1@291a6228
Now we can create our TypedDataset
:
val personDS = TypedDataset.create(people)
// personDS: TypedDataset[Person] = [age: int, birthday: bigint]
Another example
Let's define a sealed family:
sealed trait Gender
case object Male extends Gender
case object Female extends Gender
case object Other extends Gender
And a simple case class:
case class Person(age: Int, gender: Gender)
val people = Seq(Person(42, Male))
// people: Seq[Person] = List(Person(42, Male))
Again if we try to create a TypedDataset
, we get a compilation error.
val personDS = TypedDataset.create(people)
// error: could not find implicit value for parameter encoder: frameless.TypedEncoder[repl.MdocSession.MdocApp1.Person]
Let's define an injection instance for Gender
:
import frameless._
implicit val genderToInt: Injection[Gender, Int] = Injection(
{
case Male => 1
case Female => 2
case Other => 3
},
{
case 1 => Male
case 2 => Female
case 3 => Other
})
// genderToInt: Injection[Gender, Int] = frameless.Injection$$anon$1@75fb1dc5
And now we can create our TypedDataset
:
val personDS = TypedDataset.create(people)
// personDS: TypedDataset[Person] = [age: int, gender: int]
Alternatively, an injection instance can be derived for sealed families such as Gender
using the following
import, import frameless.TypedEncoder.injections._
. This will encode the data constructors as strings.
Known issue: An invalid injection instance will be derived if there are data constructors with the same name. For example, consider the following sealed family:
sealed trait Foo
object A { case object Bar extends Foo }
object B { case object Bar extends Foo }
A.Bar
and B.Bar
will both be encoded as "Bar"
thereby breaking the law that invert(apply(x)) == x
.