I'm getting started with generic programming in Scala and I'm trying to design a flexible buildable schema type (a generic description of ADTs) that can be translated into third party serialization (e.g., circe, upickle) or schema (e.g., tapir) type class instances. When building schemas for products and coproducts, it has to be able to resolve schemas for subcomponents.
The way I would like to achieve this is with a Provider type class, that will "provide" an instance of a given type either by resolving a Deriver or by resolving an existing implicit instance. By making the derivation provider a lower priority method, it should prefer instances to derivations.
Below is a version of my solution applied to a simpler case: generating a NamesFor[T] instance that contains the extracted field names of type T.
import shapeless._
import shapeless.labelled.FieldType
trait NamesFor[ T ] {
type Names
def names : Names
}
object NamesFor {
type Aux[ T, Names0 ] = NamesFor[ T ] { type Names = Names0 }
}
trait Deriver[ From, To ] {
def derive : To
}
object Deriver {
implicit def deriveNameFromSymbol[ S <: Symbol ](
implicit wit : Witness.Aux[ S ],
) : Deriver[ S, String ] =
new Deriver[ S, String ] {
override def derive : String = wit.value.name
}
implicit def deriveNameFromFieldType[ K <: Symbol, T ](
implicit deriver : Deriver[ K, String ],
) : Deriver[ FieldType[ K, T ], String ] = new Deriver[ FieldType[ K, T ], String ] {
override def derive : String = deriver.derive
}
// Deriving HList
implicit val deriveFromHnil : Deriver[ HNil, HNil ] =
new Deriver[ HNil, HNil ] {
override def derive : HNil = HNil
}
implicit def deriveNameHListFromHList[ Head, Tail <: HList, Res <: HList ](
implicit
headDeriver : Lazy[ Deriver[ Head, String ] ],
tailDeriver : Deriver[ Tail, Res ],
) : Deriver[ Head :: Tail, String :: Res ] = new Deriver[ Head :: Tail, String :: Res ] {
override def derive : String :: Res = headDeriver.value.derive :: tailDeriver.derive
}
// Here's the deriver we're interested in
implicit def deriveNamesFromLabelledGeneric[ T, R <: HList, Names0 <: HList ](
implicit
lGenEv : LabelledGeneric.Aux[ T, R ],
rDer : Deriver[ R, Names0 ],
) : Deriver[ T, NamesFor[ T ] ] = new Deriver[ T, NamesFor[ T ] ] {
override def derive : NamesFor.Aux[ T, Names0 ] = new NamesFor[ T ] {
type Names = Names0
override def names : Names = rDer.derive
}
}
}
trait Provider[ T ] {
def provide : T
}
object Provider {
implicit def provideInstance[ T ](
implicit inst : T,
) : Provider[ T ] = new Provider[T] {
override def provide : T = inst
}
}
trait LowPriorityProvider {
implicit def provideDerivation[ From, To ](
implicit
deriver : Deriver[ From, To ],
) : Provider[ To ] = new Provider[ To ] {
override def provide : To = deriver.derive
}
}
I can derive instances in the following two ways:
1: implicit provider of existing NamesFor[T] instance
case class Test( int : Int )
implicit val namesInst : NamesFor[ Test ] = new NamesFor[Test] {
override type Names = String :: HNil
override def names : String :: HNil = "INT_FIELD" :: HNil
}
val provider = implicitly[ Provider[ NamesFor[ Test ] ] ]
println( provider.provide.names ) // output: INT_FIELD :: HNil
2: implicit deriver of a NamesFor[T] from T
case class Test( int : Int )
val deriver = implicitly[ Deriver[ Test, NamesFor[ Test ] ] ]
println( deriver.derive.names ) // output: int :: HNil
but it fails when I try to resolve a provider of NamesFor[T] without
an implicit NamesFor[T] instance in scope. That is, it fails to resolve
an instance by way of provideDerivation:
case class Test( int : Int )
val provider = implicitly[ Provider[ NamesFor[ Test ] ] ]
// Err: could not find implicit value for parameter e: Provider[NamesFor[Test]]
println( provider.provide.names )
Any thoughts on how I could make this all work?
You seem just to forget to make
ProviderextendLowPriorityProviderThen
compiles
https://scastie.scala-lang.org/DmytroMitin/S7LrOQocSSeMZHetCEifDA/1
Scala 2.13.8, Shapeless 2.3.9