In the previous post
I created SqlSaver
class. Later, playing with it, I found that it has
several bugs.
First of all, it doesn’t work properly with nested classes. Let’s start with a test:
case class SaleRecord(id: Int, sale: Sale, seller: String)
"save nested case classes" in {
it should val date = LocalDateTime.now
[SaleRecord].save(stm, 6)(
SqlSaverSaleRecord(1, Sale("bar", date, 42), "Shop")
) should equal(11)
verify(stm).setInt(6, 1)
verify(stm).setString(7, "bar")
verify(stm).setTimestamp(8, Timestamp.valueOf(date))
verify(stm).setBigDecimal(9, java.math.BigDecimal.valueOf(42))
verify(stm).setString(10, "Shop")
}
Unfortunately it doesn’t compile:
[error] SqlSaverTest.scala:38: diverging implicit expansion for type SqlSaver[LocalDateTime :: BigDecimal :: HNil]
[error] starting with method hlistSaver in object SqlSaver
[error] SqlSaver[SaleRecord].save(stm, 6)(
[error] ^
Note: here and later I rewrote HLists into the infix form, for readability.
That’s strange. We know that SqlSaver for Sale
can be instantiated, because
Sale
contains only fields of supported types. Maybe shapeless cannot
construct Generic
for our nested classes? If we try to do it in REPL we get
the following result:
[SaleRecord]{type Repr = Int :: Sale :: String :: HNil } Generic
But if we try to evaluate SqlSaver[Int :: Sale :: String :: HNil]
we get an
error. The problem we are faced with is related to how Scala implicit resolution
works. This topic is described in “The Type Astronaut’s Guide to Shapeless”. The
main idea is that the Scala compiler tries to avoid infinite loops during implicit
resolution. To do that, it has several heuristics. One of them is to stop
searching if it meets the same step twice. Another one is to stop if the
complexity of type parameters is increasing for the type constructor it met
before. In shapeless one of the type constructors is ::[H, T]
– the
constructor of HList. In our case we get a more complex HList for Sale than for
SaleRecord, so it cannot find an implicit instance of SqlSaver[Sale]
and doesn’t compile.
Fortunately shapeless has special type Lazy
to solve this problem (else shapeless
would be a quite useless thing). Let’s fix the last error case:
implicit def hlistSaver[H, T <: HList](implicit
: Lazy[SqlSaver[H]],
hSaver: SqlSaver[T]
tSaver): SqlSaver[H :: T] = createSaver {
case (h :: t, stm, idx) =>
.value.save(stm, idx)(h)
hSaver.save(stm, idx + 1)(t)
tSaver}
Once we wrapped the hSaver
in Lazy
it prevents the compiler from being too
clever, and postpones the implicit parameters evaluation to runtime. Now the
SqlSaver
for HList works properly. We can fix the genericSaver
in the
same way, wrapping saver
into Lazy
:
implicit def genericSaver[A, R](implicit
: Generic.Aux[A, R],
gen: Lazy[SqlSaver[R]]
saver): SqlSaver[A] =
createSaver((v, stm, idx) => saver.value.save(stm, idx)(gen.to(v)))
Now the test compiles successfully but fails on runtime with “9 did not equal
11” message. What happened? The current implementation of HList saver assumes that the head
saver takes only one element. This worked for primitive types, but of course
doesn’t work for classes. To fix that we need to use the next index returned by
hSaver
:
implicit def hlistSaver[H, T <: HList](implicit
: Lazy[SqlSaver[H]],
hSaver: SqlSaver[T]
tSaver): SqlSaver[H :: T] = createSaver {
case (h :: t, stm, idx) =>
val next = hSaver.value.save(stm, idx)(h)
.save(stm, next)(t)
tSaver}
Now it works fine for nested classes.