NietzscheanAI
NietzscheanAI

Reputation: 966

Polymorphic Scala Shapeless mapping with two type parameters

Given traits for ProblemParser and Solver:

trait ProblemParser[Problem] {
  def parse(description: String): Option[Problem]
}

trait Solver[Problem,Solution] {
  def solve(problem: Problem): Option[Solution]
}

and HLists of parsers and solvers, I'm trying to apply all (type-appropriate) solvers to all the different Problem types that result from successful parses.

I can see how to obtain a HList of Problem Options with ~> :

object parseFn extends (ProblemParser ~> Option) {
  def apply[P](x: ProblemParser[P]): Option[P] = x.parse(input)
}

Q. Given a HList of parsers of different Problem types, how do I then map the solvers over the list of parsed problems? Presumably because Solver takes two type parameters this requires Poly1 rather than ~>?

Upvotes: 1

Views: 214

Answers (1)

Peter Neyens
Peter Neyens

Reputation: 9820

Your question was a little concise so I split it in a couple of parts to my understanding :

  1. The ProblemParser type class which can parse a description to a Problem.

    String => Option[Problem]

  2. A way to parse a list of descriptions to an HList of Problems using ProblemParser.

    eg List[String] => Option[ProblemA] :: Option[ProblemB] :: HNil

  3. The type class Solver which can give a Solution for a Problem.

    Problem => Option[Solution]

  4. Solving the Problems from step 2 using Solver.

    eg Option[ProblemA] :: Option[ProblemB] :: HNil => Option[SolutionA] :: Option[SolutionB] :: HNil


We start with defining two simple problems, getting the sum or the maximum of a pair of integers :

case class Sum(a: Int, b: Int)
case class Max(a: Int, b: Int)

Now, we create the ProblemParser type class with two instances for our two problems :

import scala.util.Try

trait ProblemParser[Problem] extends Serializable {
  def parse(description: String): Option[Problem]
}

object ProblemParser {
  def apply[A](implicit pp: ProblemParser[A]): ProblemParser[A] = pp

  def fromFunction[A](f: String => Option[A]): ProblemParser[A] =
    new ProblemParser[A] { def parse(s: String): Option[A] = f(s) }

  def intPairParser[A](f: (Int, Int) => A): ProblemParser[A] =
    fromFunction { s => 
      s.split(",") match { 
        case Array(l, r) => 
          for { 
            ll <- Try(l.toInt).toOption
            rr <- Try(r.toInt).toOption
          } yield f(ll, rr)
        case _ => None
      }
    }

  implicit val sumParser: ProblemParser[Sum] = intPairParser(Sum.apply)
  implicit val maxParser: ProblemParser[Max] = intPairParser(Max.apply)
}

Parsing the descriptions to an HList of Problems is similar to my answer in a different question :

import shapeless._
import scala.collection.GenTraversable

trait FromTraversableParsed[L <: HList] extends Serializable {
  type Out <: HList
  def apply(l: GenTraversable[String]): Out
}

object FromTraversableParsed {
  def apply[L <: HList]
    (implicit from: FromTraversableParsed[L]): Aux[L, from.Out] = from

  type Aux[L <: HList, Out0 <: HList] = FromTraversableParsed[L] { type Out = Out0 }

  implicit val hnilFromTraversableParsed: Aux[HNil, HNil] = 
    new FromTraversableParsed[HNil] {
      type Out = HNil
      def apply(l: GenTraversable[String]): Out = HNil
    }

  implicit def hlistFromTraversableParsed[H, T <: HList, OutT <: HList](implicit 
    ftpT: FromTraversableParsed.Aux[T, OutT],
    parseH: ProblemParser[H]
  ): Aux[H :: T, Option[H] :: OutT] = 
    new FromTraversableParsed[H :: T] {
      type Out = Option[H] :: OutT
      def apply(l: GenTraversable[String]): Out =
        (if(l.isEmpty) None else parseH.parse(l.head)) :: ftpT(l.tail)
    }
}

We can now parse some descriptions :

val parse = FromTraversableParsed[Max :: Sum :: HNil]
parse(List("1,2", "1,2"))  // Some(Max(1,2)) :: Some(Sum(1,2)) :: HNil

On to the Solver type class (I made Solution a dependent type) :

trait Solver[Problem] extends Serializable {
  type Solution
  def solve(problem: Problem): Option[Solution]
}

object Solver {
  def apply[Problem]
    (implicit solver: Solver[Problem]): Aux[Problem, solver.Solution] = solver

  type Aux[Problem, Solution0] = Solver[Problem] { type Solution = Solution0}

  implicit val solveMax: Aux[Max, Int] =
    new Solver[Max] { 
      type Solution = Int
      def solve(max: Max) = Some(math.max(max.a, max.b))
    }

  implicit val solveSum: Aux[Sum, Int] =
    new Solver[Sum] { 
      type Solution = Int
      def solve(sum: Sum) = Some(sum.a + sum.b)
    }
}

With a HList of Problems and Solver instances for these problems, we should be able to map over our HList and use the correct Solvers to solve the Problems :

object solve extends Poly1 {
  implicit def apply[Problem, Solution](
    implicit solver: Solver.Aux[Problem, Solution]
  ): Case.Aux[Option[Problem], Option[Solution]] =
    at[Option[Problem]](_.flatMap(solver.solve))
}

import shapeless.ops.hlist.Mapper

def parseAndSolve[Problems <: HList] = 
  new PartiallyAppliedParseAndSolve[Problems]

class PartiallyAppliedParseAndSolve[Problems <: HList] {
  def apply[OP <: HList](descriptions: List[String])(implicit 
    ftp: FromTraversableParsed.Aux[Problems, OP], 
    mapper: Mapper[solve.type, OP]
  ): mapper.Out = mapper(ftp(descriptions))
}

With all this machinery in place we can now parse a list of descriptions and solve the parsed problems :

parseAndSolve[Max :: Sum :: HNil](List("1,2", "1,2"))
// Option[Int] :: Option[Int] :: HNil = Some(2) :: Some(3) :: HNil

Upvotes: 1

Related Questions