12
votes

I'd like to somehow get at compile time the name of a field of a case class in a val (possibly a singleton-typed string or symbol?).

Something like the following:

import shapeless._
case class MyClass(field1: String, field2: Int)
val field1Lens = lens[MyClass] >> 'field1 
// val name = field1Lens.name // it should be "field1", aka 'field1.name

I don't have to necessarily use lenses, any technique that works is fine (something with LabelledGeneric?). I would like to have something where I can get the name of the case class field without specifying it separately. This way, if I refactor the name of the field1 member in the class, name changes accordingly.

Of course the following doesn't work because the macro doesn't know at compile time the name of the symbol:

val name = 'field1
val field1Lens = lens[MyClass] >> name // can't possibly work

I tried lens[MyClass] >> name.narrow but it doesn't work either

This is what I'm currently doing, and of course I don't like it:

// If I change the name of the field, compilation fails 
// and I'm forced to check this line of code so I can change the string in the line below
protected val fieldNameCheck = lens[X].someField
val someField = "someField"

edit: Ok, I gave a look at gabriele's question, and by using Keys I'm able to get an HList containing the (tagged) keys of the record. What I need though is getting one specific field, not a list containing all of them.

I'm trying to use select to get a particular key but I haven't succeeded so far

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

case class Foo(bar: String, baz: Boolean)
val labl = LabelledGeneric[Foo]
val keys = Keys[labl.Repr].apply

// the following doesn't work
// val bar = 'bar.narrow
// keys.select[bar.type]
// keys.get('bar)
2
I don't think I understand the motivation—it sounds like you want to write the literal 'bar in one place in your code so that you don't have to write it in another place, and I'm not sure how that helps with refactoring. If you're concerned about member names changing, the positional selectors might be a better choice.Travis Brown
long story short: serialization libraries. I don't want to be forced to write custom serializers to do what the libraries already do (writing json objects using field names as keys etc) just because I need to have the name of the field in some other place (e.g. when partially updating on elasticsearch). I only write the literal 'bar once, but if the case class changes, the code still compiles. I would need a ton of tests just to be sure that for each 'bar I need there is a (bar: T) case class param called exactly the same: one for every field of every class.Giovanni Caporaletti
@TravisBrown Where can I find some info about positional selectors?Giovanni Caporaletti
@TrustNoOne Ah, in that case I'd recommend val barKey = Witness('bar) somewhere and then e.g. lens[Foo] >> barKey, which should work just fine.Travis Brown

2 Answers

6
votes

The argument to >> is a Witness that captures the member name as a compile-time symbol. When you write >> 'bar, the symbol literal is implicitly converted to a Witness, which is usually what you want, but you can also provide one yourself:

scala> case class Foo(bar: String, baz: Boolean)
defined class Foo

scala> val barKey = shapeless.Witness('bar)
barKey: shapeless.Witness.Aux[shapeless.tag.@@[Symbol,String("bar")]] = ...

scala> shapeless.lens[Foo] >> barKey
res0: shapeless.Lens[Foo,String] = shapeless.Lens$$anon$7@344bfb60

As I mention in a comment above, you may also be interested in the positional selectors:

scala> shapeless.lens[Foo] >> shapeless.nat._1
res1: shapeless.Lens[Foo,Boolean] = shapeless.Lens$$anon$7@16e9d434

Or even just:

scala> shapeless.lens[Foo] >> 1
res2: shapeless.Lens[Foo,Boolean] = shapeless.Lens$$anon$7@4be29007

These don't require you to write the member name anywhere in your code, although you'll run into trouble if you rearrange members.

3
votes

Ok, thanks to Travis' comments I got it working:

import shapeless._
case class MyClass(field1: String, field2: Int)

def fieldName[A](fieldKey: Witness.Lt[_ <: Symbol])(implicit mkl: MkFieldLens[A, fieldKey.T]) = {
  lens[A] >> fieldKey
  fieldKey.value.name
}

println(fieldName[MyClass]('field1))