Search code examples
scalashapelessimplicits

Shapeless HList polymorphic map with an argument


Given an HList of Label[A](String) I want to map it into an HList of LabelWithValue[A](Label[A], A), where the actual values come from a Map[String, Any]. In the example below I just defined the map of values in the method, just imagine the values come from a database.

The below works, but it is very veeery hacky because it uses a global var. Instead I'd like to pass the Map[String, Any] into GetLabelWithValue. I didn't find a way though, because the caller of getValues implicitly creates a Mapper, and at that point the map of values doesn't exist yet. I tried to create a Mapper myself, but my type level programming skills aren't yet good enough.

import shapeless._
import shapeless.poly._
import shapeless.ops.hlist._

object Main extends App {
  case class Label[A](name: String)
  case class LabelWithValue[A](label: Label[A], value: A)

  // TODO: avoid the horrible global state - pass in the Map as a parameter
  var horribleGlobalState: Map[String, Any] = _
  object GetLabelWithValue extends (Label ~> LabelWithValue) {
    def apply[A](label: Label[A]) =
        LabelWithValue(label, horribleGlobalState.get(label.name).asInstanceOf[A])
  }

  val label1 = Label[Int]("a")
  val label2 = Label[String]("b")
  val labels = label1 :: label2 :: HNil
  val labelsWithValues: LabelWithValue[Int] :: LabelWithValue[String] :: HNil = getValues(labels)
  println(labelsWithValues)

  def getValues[L <: HList, M <: HList](labels: L)(
    implicit mapper: Mapper.Aux[GetLabelWithValue.type, L, M]) = {

    horribleGlobalState = Map("a" -> 5, "b" -> "five")
    labels map GetLabelWithValue
  }
}

Here is an alternative implementation of GetLabelWithValue, which behaves the same way:

object GetLabelWithValue extends Poly1 {
  implicit def caseLabel[A] = at[Label[A]] { label ⇒
    LabelWithValue(label, horribleGlobalState.get(label.name).asInstanceOf[A])
  }
}

Solution

  • I am by no means shapeless guru but here's first thing that comes to my mind:

    object Main extends App {
      case class Label[A](name: String)
      case class LabelWithValue[A](label: Label[A], value: A)
    
      object combine extends Poly2 {
        implicit def workS[A <: HList, B] = at[Label[B], (Map[String, Any], A)] {
          case (i, (map, res)) ⇒
            (map, LabelWithValue(i, map.get(i.name).asInstanceOf[B]) :: res)
        }
      }
    
      var state: Map[String, Any] = Map("a" -> 5, "b" -> "five")
    
      val label1 = Label[Int]("a")
      val label2 = Label[String]("b")
    
      val labels = label1 :: label2 :: HNil
      val mapped = labels.foldRight((state, HNil))(combine)._2
      println(mapped)
    }
    

    I'm not saying there's not better way, but this seems pretty reasonable - instead of global state you capture it using fold and decide based on it. Probably gives you a bit more power than you need though (as you could mutate the map inbetween folds, but...)