0
votes

I'm trying to use shapeless's hlist to construct introspectable URL templates, but I'm having trouble with traversing my HList. The following doesn't compile:

import shapeless.{::, HList, HNil}
import shapeless.LUBConstraint._
import shapeless.ops.hlist.ToTraversable._

import scala.util.Try
import shapeless.ops.hlist._

object Path {
  def /(s: String) = Path(PathLiteral(s) :: HNil)

  def param[T](name: String) = PathParameter(name)
}


sealed trait PathSegment[+T]
case class PathLiteral(value: String) extends PathSegment[String]
case class PathParameter[+T](name: String) extends PathSegment[T]


case class Path[L <: HList : <<:[PathSegment[_]]#λ](segments: L)
                                                   (implicit ev: ToList[L, PathSegment[_]])
{
  def /(literal: String) = Path(PathLiteral(literal) :: segments)

  def /[T](param: PathParameter[T]) = Path(param :: segments)

  override def toString: String = s"Path(${segments.toList.reverse})"
}

object Test extends App {

  import Path.param
  val myPath = Path / "v1" / "pets" / param[String]("name") / "pictures"
  println(myPath)

}

It seems to me like the ToTraversable._ import covers the HNil case and the case of having the tail of an HList and a new head with the same Least Upper Bound. Obviously I'm either missing an import or misunderstanding everything.

I'm not sure if caching the evidence in the class as an implicit parameter is kosher; I'm doing it because

  1. I don't want hlist details to leak into the external API
  2. I need it for a nice toString
1
what's the problem?flavian
It doesn't compileChad

1 Answers

0
votes

The reason this doesn't work is the implicit which can prove:

ToList[PathLiteral :: L, PathSegment[_]] given

  • ToList[L, PathSegment[L]]
  • L <: HList

is this implicit def's type signature from hlists.scala:

implicit def hlistToTraversable[H1, H2, T <: HList, LubT, Lub0, M[_]]
  (implicit
    tttvs  : Aux[H2 :: T, M, LubT],
    u      : Lub[H1, LubT, Lub0],
    cbf    : CanBuildFrom[M[Lub0], Lub0, M[Lub0]]) : Aux[H1 :: H2 :: T, M, Lub0]

actually needs to know the type of the head of the list as well as the new item (H1 and H2), so it can't be constructed from what's available inside my class.

Some workarounds:

  • build List manually alongside the HList

    case class Path[L <: HList](segments: L, segmentsList: List[PathSegment[_]]) {
      def /(literal: String) = Path(PathLiteral(literal) :: segments, PathLiteral(literal) :: segmentsList)
      def /[T](param: PathParameter[T]) = Path(param :: segments, param :: segmentsList)
      override def toString: String = s"Path(${segmentsList.reverse})"
    }
    
  • push implicit parameters out to Path's methods

    def printPath[L <: HList](path: Path[L])
                             (implicit ev: ToList[L, PathSegment[_]]) =
    path.segments.toList.reverse.collect {
      case PathLiteral(value) => URLEncoder.encode(value, "UTF-8")
      case PathParameter(name) => s"<$name>"
    }.mkString("/")
    
    printPath(Path / "v1" / "pets"  / param[String]("name") / "pictures")
    // v1/pets/<name>/pictures