scalamacrosshapeless

How to get case class field names and values as (String, String) with Shapeless or Macro


I have been struggling for a couple days already to try to create a macro or use shapeless to create a method/function to extract field names and values as a Tuple[String, String].

Lets imagine the following case class:

case class Person(name: String, age: Int)

I want to have something like this (doesn't really need to be a method in case class).

case class Person(name: String, age: Int) {
    def fields: List[(String, String)] = ???
}

// or

def fields[T](caseClass: T): List[(String, String)] = ???

I've seen quite few similar solutions here but I can't make it work with my use case of (String, String)

I would also appreciate some literature to learn and expand my knowledge regarding macros, I have both Programming in Scala(Third Edition by Martin) and Programming Scala (O'REILLY - Dean Wampler & Alex Payne) and only O'REILLY has a very small chapter regarding macros and to be honest its very lacking.

PD: I'm using Scala 2.12.12 so I don't have those fancy new methods for case class productElementNames and such :(


Solution

  • Based on LabelledGeneric and Keys type classes

    import shapeless.LabelledGeneric
    import shapeless.HList
    import shapeless.ops.hlist.ToTraversable
    import shapeless.ops.record.Keys
    
    case class Person(name: String, age: Int)
    
    def fields[P <: Product, L <: HList, R <: HList](a: P)(
      implicit
      gen: LabelledGeneric.Aux[P, L],
      keys: Keys.Aux[L, R],
      ts: ToTraversable.Aux[R, List, Symbol]
    ): List[(String, String)] = {
      val fieldNames = keys().toList.map(_.name)
      val values = a.productIterator.toList.map(_.toString)
      fieldNames zip values
    }
    
    fields(Person("Jean-Luc, Picard", 70))
    // : List[(String, String)] = List((name,Jean-Luc, Picard), (age,70))
    

    scastie

    IDEA ... shows an error ... No implicit arguments

    IntelliJ in-editor error highlighting is sometimes not 100% accurate when it comes to type-level code and macros. Best is to consider it as just guidance, and put trust in the Scala compiler proper, so if compiler is happy but IJ is not, then go with the compiler. Another options is to try Scala Metals which should have one-to-one mapping between compiler diagnostics and in-editor error highlighting.

    why you used LabelledGeneric.Aux, Keys.Aux, ToTraversable.Aux

    This is using a design pattern called type classes. My suggestion would be to work through The Type Astronaut's Guide to Shapeless in particular section on Chaining dependent functions

    Dependently typed functions provide a means of calculating one type from another. We can chain dependently typed functions to perform calculations involving multiple steps.

    Consider the following dependency between types

                    input type
                             |
    gen: LabelledGeneric.Aux[P, L],
                                |
                                output type
     
          input type
                   |
    keys: Keys.Aux[L, R]
                      |
                      output type
    

    Note how for example the output type L of LabelledGeneric becomes the input type of Keys. In this way you are showing the compiler the relationship between the types and in return the compiler is able to give your an HList representing the field names from Product representing the particular case class, and all this before the program even runs.

    ToTraversable is needed so you can get back a regular Scala List from an HList which enables the following bit

    .toList.map(_.name)
    

    Hopefully this gives you at least a little bit of direction. Some keywords to search for are: type classes, dependent types, implicit resolution, type alias Aux pattern, type members vs type parameters, type refinement, etc. Typelevel community has a new Discord channel where you can get further direction.