Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Type Based Attribute Extractor - Getter only Lens?

What is the best way to extract a type from a data container such as a case class.

For example if I have a type Tagged[U] = { type Tag = U} tagged type trait PID which is a tagged Int type ProductId = Int with Tagged[PID] or scalaz style type ProductId = Int @@ PID and say other fields in the product type Name = String @@ PNameetc and a data container which contains the attributes of a product;

case class Product(pid: ProductId, name: Name, weight: Weight)

How could I write a generic extractor A => B style method without resorting to reflection?

The reason is I want to dynamically extract a field from the Product container at runtime. I.e. the user passes in the attribute of the product they want to extract.

I.e. if I want to dynamically get ProductId could I write a method that takes the type and returns a value e.g.

trait Extractor[A] {
  def extract[B](i: A): B = //get type B from the Product class
}

Or am I over complicating things.

I could just write simple extractor class which takes A => B function and define it for each type;

trait Getter[A, B] {
  def extract(i: A): B
}
//... mix this in...
trait GetPID extends Getter[Product, ProductId] {
  override def extract(implicit i: Product) = i.pid
}
trait GetName extends Getter[Product, Name] {
  override def extract(implicit i: Product) = i.name
}

Then add them in where needed.

val dyn = new DynamicProductExtractor with GetPID 
dyn.extract

but this seems like a hassle.

I'm thing something like Lens would be useful here.

like image 661
NightWolf Avatar asked Feb 10 '23 22:02

NightWolf


1 Answers

For the sake of a complete example, suppose we have the following types and some example data:

import shapeless._, tag._

trait PID; trait PName; trait PWeight

type ProductId = Int @@ PID
type Name = String @@ PName
type Weight = Double @@ PWeight

case class Product(pid: ProductId, name: Name, weight: Weight)

val pid = tag[PID](13)
val name = tag[PName]("foo")
val weight = tag[PWeight](100.0)

val product = Product(pid, name, weight)

I'm using Shapeless's tags here, but everything below would work the same with Scalaz's or your own Tagged. Now assuming we want to be able to look up a member by type in an arbitrary case class, we can create an extractor using Shapeless's Generic:

import ops.hlist.Selector

def extract[A] = new {
  def from[C, Repr <: HList](c: C)(implicit
    gen: Generic.Aux[C, Repr],
    sel: Selector[Repr, A]
  ) = sel(gen.to(c))
}

Note that I'm using a structural type for the sake of simplicity and concision, but you could very easily define a new class that would do the same thing.

Now we can write the following:

scala> extract[ProductId].from(product)
res0: Int with shapeless.tag.Tagged[PID] = 13

Note that if the case class has more than one member with the requested type, the first will be returned. If it doesn't have any members with the right type (e.g. something like extract[Char] from(product)), you'll get a nice compile-time error.

You could use lenses here, but you'd need to write more or less this same machinery—I don't know of a lens implementation that gives you indexing by type (Shapeless for example provides positional indices and indexing by member name).

(Note that this isn't really "dynamic", since the type that you're looking up in the case class must be statically known at compile time, but that's also the case in the examples you give above.)

like image 186
Travis Brown Avatar answered Feb 16 '23 03:02

Travis Brown