Skip to content

Instantly share code, notes, and snippets.

@scottweaver
Last active July 19, 2019 15:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scottweaver/259e6262c31927d3d90f9e39d32a5a76 to your computer and use it in GitHub Desktop.
Save scottweaver/259e6262c31927d3d90f9e39d32a5a76 to your computer and use it in GitHub Desktop.
This is an example of how to extract field names generically using Shapeless's LabelledGeneric.
package vyze.funding.dwr
import simulacrum.typeclass
import shapeless._
import shapeless.ops.hlist.ToTraversable
import shapeless.ops.record._
@typeclass
trait HasColumnNames[T] {
def columnNames(t: T): List[String]
}
object HasColumnNames {
/**
* It took me a bit figure out the proper implicits needed and their associated type arguments. So, let's break it down.
*
* First the type arguments for the input `def`.
*
* - `T` The type we want to extract field names from. In most cases this will be a case class.
* - `R` This the generic representations, aka `Repr`, of type `T`. If we continue with the case class example, this will be an `HList`
* of all the fields extruded from `T`.
* - `K` This is an `HList` whose elements will always be a `scala.Symbol`. Again using the case class example, each Symbol in K
* represents an actual field name in the case class `T`.
*
* Now for the Shapeless type classes.
*
* - `LabelledGeneric.Aux[T, R]` Carries the generic representation of type `T` along with the name of each field in type `T`, assuming
* `T` is a case class. To expand on the description of `R` above, each element of the `HList` `R` is more than just the corresponding
* field in `T`, it is actually what Shapeless refers to as a record type which is encoded as a `shapeless.labelled.FieldType`.
*
* A `FieldType` is simply this:
* ```
* type FieldType[K, +V] = V with KeyTag[K, V]
* ```
*
* For case classes, the `V` is simply the value of field whilst the `K` is a Symbol representing the name of the field as defined
* on the case class itself.
*
* - `Keys.Aux[R, K]` Provides evidence that our `LabelledGeneric.Repr`, `R`, provides an `HList` of keys, `K`. Remember, `K` is
* simply carrying all of our case class's field names as `Symbols.`
* - `ToTraversable.Aux[K, List, Symbol]` This allows us to call `toList` on our `HList`, `K`, and convert it to a `List[Symbol]`.
*
*/
implicit def GenericHasColumnNames[T, R <: HList, K <: HList](
implicit ev0: LabelledGeneric.Aux[T, R],
ev1: Keys.Aux[R, K],
ev3: ToTraversable.Aux[K, List, Symbol]
): HasColumnNames[T] = { t: T =>
def camel2Underscore(text: String) = text.drop(1).foldLeft(text.headOption.map(_.toLower + "") getOrElse "") {
case (acc, c) if c.isUpper => acc + "_" + c.toLower
case (acc, c) => acc + c
}
val gen = LabelledGeneric[T]
val fieldNames = Keys[gen.Repr].apply()
fieldNames.toList.map(fn => camel2Underscore(fn.name))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment