12

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)
Community
  • 1
  • 1
Giovanni Caporaletti
  • 5,426
  • 2
  • 26
  • 39
  • Possible duplicate of [Extract label values from a LabelledGeneric instance](http://stackoverflow.com/questions/27434302/extract-label-values-from-a-labelledgeneric-instance) – Gabriele Petronella Mar 15 '16 at 09:28
  • 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 Mar 15 '16 at 15:02
  • 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 Mar 15 '16 at 15:41
  • @TravisBrown Where can I find some info about positional selectors? – Giovanni Caporaletti Mar 15 '16 at 16:17
  • @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 Mar 15 '16 at 16:52
  • The positional selectors are e.g. `lens[Foo] >> 1` (where the selector is an index, not a member name). – Travis Brown Mar 15 '16 at 16:53
  • @TravisBrown ah! Witness('bar)! It works, you can put it in answer if you want ;) – Giovanni Caporaletti Mar 15 '16 at 17:00

2 Answers2

7

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.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
4

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))
Giovanni Caporaletti
  • 5,426
  • 2
  • 26
  • 39
  • I think you can do without the `lens[A] >> fieldKey` as it's unassigned. The compiler resolving the `MkFieldLens` instance is enough for making sure the field is on the class. – Matthew Pickering Oct 15 '17 at 11:56