Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to infer inner type of Shapeless record value with unary type constructor?

I'm having trouble understanding the way the Shapeless record Selector interacts with scala's type inference. I'm trying to create a method that can grab a field from a Shapeless record by key, only if the value of the field has a certain unary type constructor, in this particular case Vector[_], and then grab an inner value of inferred type V out of the Vector, in this case with Vector.apply().

I feel like I'm close. This works, with a concrete inner type of Int:

val record = ( "a" ->> Vector(0,2,4) ) :: ( "b" ->> Set(1,3,5) ) :: HNil

def getIntFromVectorField[L <: HList](l: L, fieldName:Witness, index:Int)(implicit 
  sel: Selector.Aux[L, fieldName.T, Vector[Int]]
):Int = l(fieldName).apply(index)

getIntFromVectorField(record,"a",1) // Returns 1
getIntFromVectorField(record,"b",0) // Does not compile, as intended

But if I try to infer the inner type, it fails:

def getValueFromVectorField[L <: HList,V](l:L, fieldName:Witness, index:Int)(implicit 
  sel: Selector.Aux[L,fieldName.T,Vector[V]]
):V = l(fieldName).apply(index) // Compiles
getValueFromVectorField(record,"a",1) // Why does this not compile? 

Here's the full error:

could not find implicit value for parameter sel: 
shapeless.ops.record.Selector[shapeless.::[scala.collection.immutable.Vector[Int] 
with shapeless.labelled.KeyTag[String("a"),scala.collection.immutable.Vector[Int]],
shapeless.::[scala.collection.immutable.Set[Int] 
with shapeless.labelled.KeyTag[String("b"),scala.collection.immutable.Set[Int]],
shapeless.HNil]],String("a")]{type Out = scala.collection.immutable.Vector[V]}

What I have been able to do instead is this:

def getValueFromVectorField[L <: HList,T,V](l:L, fieldName:Witness, index:Int)(implicit 
  sel: Selector.Aux[L,fieldName.T,T], 
  unpack: Unpack1[T,Vector,V]
):V = l(fieldName) match { 
  case v:Vector[V] => v.apply(index) 
}
getValueFromVectorField(record,"a",1) // Returns 1, Yay!
getValueFromVectorField(record,"b",0) // Does not compile, as intended

Which should be safe, yes? But the pattern matching doesn't feel very idiomatic for shapeless, and I'm wondering why the more concise approach with inference doesn't work. Is there a cleaner way to do this?

like image 656
Richard Whaling Avatar asked Oct 28 '16 15:10

Richard Whaling


1 Answers

Scala is really bad about type inference in cases like this (where you want to unify the result of a functional dependency and something like Vector[V] and have the V inferred).

You can help the compiler through the process by breaking down the steps:

import shapeless._, ops.record.Selector, syntax.singleton._

def getValueFromVectorField[L <: HList, VS, V](
  l: L,
  fieldName: Witness,
  index: Int
)(implicit
  sel: Selector.Aux[L, fieldName.T, VS],
  ev: VS <:< Vector[V]
): V = sel(l).apply(index)

val record = ( "a" ->> Vector(0,2,4) ) :: ( "b" ->> Set(1,3,5) ) :: HNil

getValueFromVectorField(record,"a",1) // Returns 1, Yay!
getValueFromVectorField(record,"b",0) // Does not compile, as intended

Now it'll infer VS first and then figure out that VS is a subtype of Vector[V], instead of having to do both in one step.

This is exactly the same thing that your Unpack1 version does, except that Unpack1 only proves that T is Vector[V]—it doesn't actually give you a way to get a Vector[V] from a T (unlike <:<, which does).

So your Unpack1 version is safe, in the sense that you can convince yourself that it provides all the pieces of evidence you need, but they're not in a form the compiler understands, so you have to downcast with the type case in the pattern match. <:< is better because the compiler does understand it, but also because it's more recognizable as a workaround for this limitation, since it's provided by the standard library, etc.

like image 138
Travis Brown Avatar answered Oct 28 '22 06:10

Travis Brown