Search code examples
scalashapeless

Using shapeless to extract data from case classes, modify it, and recreate the case class


I have a program with some case classes. One of them might be a Person, which relies on some other classes, including one called StemmedString which will be important here:

case class Person(name: String, skills: List[Skill], hobbbies: List[StemmedString]
case class Skill(score: Int, title: StemmedString)
case class StemmedString(original: String, stemmed: String)

Now I want to be able to translate this person into another language. To do that, I want to go from Person -> List[String]. That's a list of single strings that need to be translated. Then I want to go BACK from List[String] -> Person.

I want to be able to define all the TYPES of things that should be translated, without needing to know the format of Person in advance, so that this is generalizable over multiple case classes that are made up of the same TYPES that can be translated.

Let's say what we translate is all the StemmedString's.

I can LabelledGeneric and flatMap to create two HLists, one of values to be translated, the other of values that will not be translated:

trait LowPriorityUnTranslatable extends Poly1 {
  implicit def default[T] = at[T](_ :: HNil)
}

object unTranslatable extends LowPriorityUnTranslatable {
  implicit def caseStemmedString[K, T] = at[FieldType[K, StemmedString]](x => HNil)
  implicit def caseSkill[K, T] = at[FieldType[K, Skill](x => HNil)
}


trait LowPriorityTranslatable extends Poly1 {
  implicit def default[T] = at[T](HNil)
}

object Translatable extends LowPriorityTranslatable {
  implicit def caseStemmedString[K, T] = at[FieldType[K, StemmedString]](_ :: HNil)
  implicit def caseSkill[K, T] = at[FieldType[K, Skill](_ :: HNil)
}

This feels a bit verbose, but works nicely. I now have two HLists, and they can easily be concatenated and returned back to the original case class using align:

val person = Person("...")
val gen = LabelledGeneric[Person] 
val personList = gen.to(person)
val toTranslate = personList flatMap isTranslatable
val notTranslated = personList flatMap unTranslatable    
gen.from((toTranslate ++ notTranslated) align personList)

This is very cool, now all I need to do is add a translation step in the middle. It's easy to go from isTranslatable -> List[String] of course, however I cannot quite figure out how to do this in a way where I can go back again. I started trying to learn about shapeless because it seemed like the right tool for this situation, but I don't quite understand how to use it fully. In my head, if I can solve this question, then I will be fine, but it could be that there is a much easier way to use shapeless to solve this problem. Any insight would be much appreciated!


Solution

  • If you don't mind using mutable data structures (in this case an Iterator), you can use everything/everywhere for an easy solution.

    First, we extract the to-be-translated strings using an everything query; in this example we are extracting the original values of the StemmedString objects. The singleton lists are concatenated using the Append function.

    trait TranslatableLP extends Poly1 {
      implicit def default[T] = at[T](_ => Nil: List[String])
    }
    
    object Translatable extends TranslatableLP {
      implicit def caseStemmedString = at[StemmedString](s => List(s.original))
    }
    
    object Append extends Poly2 {
      implicit val caseString = at[List[String], List[String]](_ ++ _)
    }
    
    val strings = everything(Translatable)(Append)(person)
    

    Now we translate the strings:

    def translate(s: String): String = ???
    
    val translatedStrings = strings.map(translate)
    

    And finally we can map the StemmedString objects using the translated strings using everywhere:

    object Update extends Poly1 {
      def iterator = translatedStrings.iterator
      implicit def caseStemmedString = at[StemmedString](_.copy(original = iterator.next))
    }
    
    val translated = everywhere(Update)(person)
    

    When I find some time I'll try to come up with a cleaner solution using only immutable data structures.