envy
envy

Reputation: 127

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 :(

Upvotes: 2

Views: 1715

Answers (1)

Mario Galic
Mario Galic

Reputation: 48400

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.

Upvotes: 3

Related Questions