Search code examples
scalashapeless

How to get the name of a case class field as a string/symbol at compile time using shapeless?


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)

Solution

  • 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.