2

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.

Thank you!

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

envy
  • 127
  • 3
  • 10

1 Answers1

2

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.

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • Your solution worked perfectly. Thank you! Questions: 1) IDEA Idea is not able to see implicit Attributes[A] so despite compilation is ok, it shows an error like this: No implicit arguments of type: Attributes[CaseClass] anything we might need to add? or just ignore? 3)I think I get the general idea of what your code, but I'd love to understand it more, would be possible for you to give an explanation of what it actually does and why you used LabelledGeneric.Aux, Keys.Aux, ToTraversable.Aux, trait Attributes[T] and so on? I'd love to be able to craft solutions like yours! Thank you really! – envy May 14 '21 at 13:34
  • 1
    @envy Please see edited answer. I have tried to simplify the solution and give some further pointers. – Mario Galic May 14 '21 at 20:07
  • Thank you! @mario-galic really appreciate your detailed answer now I understand it a little more. I'll make sure to read that book and search for the topics you mentioned! Have a great weekend! – envy May 15 '21 at 12:14