scalashapelesshlist

How do I map over an HList where all of the elements are instances of a typeclass?


Let's say I have a typeclass such as this one:

trait Select[A] {
  def select(selector: String): Set[A]
}

The typeclass provides the functionality "given a selector string, gimme a set of A objects". However, it quickly gets tiring when you need to do this multiple times:

val setOfGrapes = Select[Grape].select("red-seedless")
val setOfApples = Select[Apple].select("fuji-apple")
// and so on...

My program lets users input strings that contain multiple selectors, so this all happens at runtime.

In the interest of making this more terse, how could I use shapeless to write a function that can be used like this?

val setOfGrapes :: setOfApples :: HNil =
  selectMultiple("red-seedless, fuji")(Select[Grape] :: Select[Apple] :: HNil)

The motivation is being able to quickly chain multiple Selects together. In essence, I am going from Select[Grape] :: Select[Apple] :: HNil to Set[Grape] :: Set[Apple] :: HNil by running a method from the Select.

Ignoring the string processing (handling commas, globs, and other things), how would I go about implementing this?

I've tried this:

object fun extends Poly1 {
  implicit def selectCase[A] = at[Select[A]](_.select(""))
}

def mapFun[H <: HList](inputs: H) = inputs map fun

The compiler tells me, could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper[fun.type,H]. I assume this is because it cannot guarantee that every element of the HList is a Select[A]. How do I tell the compiler this?

I've tried specifying a shapeless.UnaryTCConstraint[H, Select] like this:

def mapFun[H <: HList](inputs: H)(implicit ev: shapeless.UnaryTCConstraint[H, Select]) = inputs map fun

But I receive the same error.

A friend recommended I use Comapped, Mapper, and ~>, so I tried this:

object mapper extends (Select ~> Set) {
  override def apply[A](s: Select[A]): Set[A] = s.select(???)
}

def mapFun[II <: HList, OO <: HList](inputs: II)(
  implicit ev: Comapped.Aux[II, Select, OO],
  ev2: Mapper.Aux[mapper.type, II, OO]
): OO = input map fun

But the compiler could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper[ammonite.$sess.cmd8.fun.type,II].


Solution

  • You should basically just add the required implicit evidence to your mapFun method:

    object fun extends Poly1 {
      implicit def selectCase[A] = at[Select[A]](_.select(""))
    }
    
    def mapFun[H <: HList](inputs: H)(implicit m: Mapper[fun.type, H]) = inputs map fun
    
    scala> mapFun(Select[Apple] :: Select[Grape] :: Select[Apple] :: HNil)
    val res1: scala.collection.immutable.Set[Apple] :: scala.collection.immutable.Set[Grape] :: scala.collection.immutable.Set[Apple] :: shapeless.HNil = Set(Apple@44ebbbe8) :: Set(Grape@76eae32c) :: Set(Apple@666f67b6) :: HNil
    

    To get your preferred API you can introduce a "manually curried" function like this:

    class SelectMultipleApply(selector: String) {
      def apply[H <: HList](inputs: H)(implicit m: Mapper[fun.type, H]) = inputs map fun
    
      object fun extends Poly1 {
        implicit def selectCase[A] = at[Select[A]](_.select(selector))
      }
    }
    
    def selectMultiple(selector: String) = new SelectMultipleApply(selector)
    
    scala> selectMultiple("red-seedless, fuji")(Select[Grape] :: Select[Apple] :: HNil)
    val res5: scala.collection.immutable.Set[Grape] :: scala.collection.immutable.Set[Apple] :: shapeless.HNil = Set(Grape@63c81efa) :: Set(Apple@29dbae38) :: HNil