Putting out the Bonfire of the Arities with Shapeless?

117 views Asked by At

I have a situation where I want to abstract over arity, and establish type agreement between one or more "raw" types (A and B below), a method that should return a corresponding Seq(Option[A], Option[B], ...) of those types (named extract below), and a set of field configurations (named configs below), each of which knows how to get a value of the corresponding "raw" type.

In the code below, ideally I'd like Dimensions1 and Dimension2 to not exist. If I had to do some kind of s.c.i.List-like recursive head/tail construct, I would be OK with that. :)

/***
scalaVersion := "2.11.4"

libraryDependencies := Seq("com.chuusai" %% "shapeless" % "2.0.0")
*/
object Main extends App {
  import shapeless._

  case class JsonEventRecord()

  case class DimensionConfig[T](name: String,
                                valueSource: JsonEventRecord => Option[T]) {
    def extract(rec: JsonEventRecord): Option[T] = {
      valueSource(rec)
    }
  }

  trait Dimensions {
    type All <: HList // I'd like to constrain this to (Option[A], Option[B], ...)
    type Configs <: HList // I'd like to constrain this to (DimensionConfig[A],     DimensionConfig[B], ...)

    def configs: Configs
    def extractAll(rec: JsonEventRecord): All
  }

  // I'd like this to not exist :)
  trait Dimensions1 extends Dimensions {
    type A

    type All = Option[A] :: HNil
    type Configs = DimensionConfig[A] :: HNil

    val config1: DimensionConfig[A]
    def configs = config1 :: HNil

    override def extractAll(rec: JsonEventRecord): All = HList(config1.extract(rec))
  }

  // I'd like this to not exist :)
  trait Dimensions2 extends Dimensions {
    type A
    type B

    type All = Option[A] :: Option[B] :: HNil
    type Configs = DimensionConfig[A] :: DimensionConfig[B] :: HNil

    val config1: DimensionConfig[A]
    val config2: DimensionConfig[B]
    def configs = config1 :: config2 :: HNil

    override def extractAll(rec: JsonEventRecord): All = {
      HList(
        config1.extract(rec),
        config2.extract(rec)
      )
    }
  }
}
1

There are 1 answers

0
Alex Archambault On

If I understand the problem well, you would like a function that given an HList of DimensionConfig[T] would give you a Dimension with the type parameters All and Configs set to the right types and an implementation of extractAll. (Correct me if I'm wrong :-)

So a dependent function à la shapeless should be able to provide that:

trait DimensionsOf[L <: HList] extends DepFn1[L] {
  type All <: HList
  type Out = Dimensions.Aux[All, L]
}

def dimensions[L <: HList](configs: L) 
 (implicit dimensionsOf: DimensionsOf[L]): dimensionsOf.Out =
  dimensionsOf(configs)

The above defines a "pseudo" dependent function, that accepts a HList as argument (we will later make it accept only HLists made of DimensionConfig[T]), and returns a Dimensions with its type parameters set (see below for the definition of Dimensions.Aux - we will make All be a HList made of Option[T] matching the input HList types).

We then have to provide a definition of this dependent function at some values, here HLists made of DimensionsConfig[T] - this is typically made in the dependent function trait's singleton object:

object DimensionsOf {
  type Aux[L <: HList, All0 <: HList] = DimensionsOf[L] { type All = All0 }

  implicit val dimensionsOfHNil: DimensionsOf.Aux[HNil, HNil] =
    new DimensionsOf[HNil] {
      type All = HNil
      def apply(l: HNil) = new Dimensions {
        type All = HNil
        type Configs = HNil
        val configs = HNil
        def extractAll(rec: JsonEventRecord) = HNil
      }
    }

    implicit def dimensionsOfHCons[H, CT <: HList]
     (implicit dimensionsOfTail: DimensionsOf[CT]): Aux[DimensionConfig[H] :: CT, Option[H] :: dimensionsOfTail.All] =
      new DimensionsOf[DimensionConfig[H] :: CT] {
        type All = Option[H] :: dimensionsOfTail.All
        def apply(l: DimensionConfig[H] :: CT) = new Dimensions {
          type All = Option[H] :: dimensionsOfTail.All
          type Configs = DimensionConfig[H] :: CT
          val configs = l
          val tailDimensions = dimensionsOfTail(l.tail)
          def extractAll(rec: JsonEventRecord) = l.head.extract(rec) :: tailDimensions.extractAll(rec)
        }
      }
  }

Here we defined DimensionsOf at HNil, and HLists of the form DimensionConfig[H] :: CT, where CT is an HList at which DimensionsOf is it-self defined (as we require - and then use - an implicit that attests so).

Dimensions.Aux used in the above definitions is defined like

object Dimensions {
  type Aux[All0 <: HList, Configs0 <: HList] = Dimensions { type All = All0; type Configs = Configs0 }
}

The dimensions function defined above can then be used this way:

val intConfig = DimensionConfig[Int]("Int", _ => Some(2))
val stringConfig = DimensionConfig[String]("String", _ => Some("a"))

val dimensions1 = dimensions(intConfig :: HNil)
val res1 = dimensions1.extractAll(JsonEventRecord()) // Option[Int] :: HNil

val dimensions2 = dimensions(intConfig :: stringConfig :: HNil)
val res2 = dimensions2.extractAll(JsonEventRecord()) // Option[Int] :: Option[String] :: HNil

Dimensions1 and Dimensions2 are not needed any more! And you can get Dimensions for arbitrary arities at the same time, e.g.:

val dimensions4 = dimensions(intConfig :: stringConfig :: intConfig :: stringConfig :: HNil)
val res4 = dimensions4.extractAll(JsonEventRecord()) // Option[Int] :: Option[String] :: Option[Int] :: Option[String] :: HNil