Return most specific type given a method parameter

Viewed 356

I am trying to create a specific and unique type given some variable parameter.

Listed below you find the prediined types representing the first four natural numbers (with an HList like approach of Digit0..Digit9, DimensionCons and EndDimension (like HNil)).

  object Defined {
    type D1 = DimensionCons[Digit1, EndDimension.type]
    val d1 = DimensionCons(Digit1(), EndDimension)

    type D2 = DimensionCons[Digit2, EndDimension.type]
    val d2 = DimensionCons(Digit2(), EndDimension)

    type D3 = DimensionCons[Digit3, EndDimension.type]
    val d3 = DimensionCons(Digit3(), EndDimension)

    type D4 = DimensionCons[Digit4, EndDimension.type]
    val d4 = DimensionCons(Digit4(), EndDimension)
  }

I am seeking for a method with an indicated signature like

  def getDimensionTpe[D <: Dimension](dim: Int) : D

that e.g. returns DimensionCons[Digit2, EndDimension.type] for getDimensionTpe(2).

Questions:

  • Is this possible without a whitebox macro (that generates the types?) If so, how and what to take care of?
  • Are there alternative techniques possible or applicable?
  • Can path dependent types help?

Thanks, Martin

1 Answers

I'm not sure I understand what exactly you're doing with DimensionCons and the DigitN business, but it is possible to use Shapeless to write a method that will take a integer (although only a literal constant—it must be known at compile-time) and have that integer determine the static type of the return value.

To simplify your code for the sake of having a complete working example, suppose we want to write a method like the following:

trait Dimension
case class Dim1() extends Dimension
case class Dim2() extends Dimension
case class Dim3() extends Dimension
// And so on...

def getDimensionTpe[D <: Dimension](dim: Int) : D

…where getDimensionTpe(1) would return Dim1() (statically typed as Dim1), getDimensionTpe(2) would return Dim2(), etc. To do this we can introduce a type class mapping between Shapeless natural numbers and dimensions:

import shapeless.{ DepFn0, Nat }

trait DimMap[N <: Nat] extends DepFn0 {
  type Out <: Dimension
}

object DimMap {
  type Aux[N <: Nat, D <: Dimension] = DimMap[N] { type Out = D }

  implicit val dimMap1: Aux[Nat._1, Dim1] = new DimMap[Nat._1] {
    type Out = Dim1
    def apply(): Dim1 = Dim1()
  }

  implicit val dimMap2: Aux[Nat._2, Dim2] = new DimMap[Nat._2] {
    type Out = Dim2
    def apply(): Dim2 = Dim2()
  }

  implicit val dimMap3: Aux[Nat._3, Dim3] = new DimMap[Nat._3] {
    type Out = Dim3
    def apply(): Dim3 = Dim3()
  }

  // And so on as needed.
}

If you have more structure for your dimension type, it might be possible to avoid the boilerplate here, but it's not clear to me how your DigitN stuff is working. That seems orthogonal to the main point of the question, though, about how to define getDimensionTpe.

You could define it as something with a signature very much like the following, if you didn't mind writing a white-box macro:

def getDimensionTpe[D <: Dimension](dim: Int) : D

That's a huge pain in the ass, though, and Shapeless lets you write something that's essentially the same without custom macros. Given the DimMap type class above, you can write the following:

def getDimensionTpe(dim: Nat)(implicit m: DimMap[dim.N]) : m.Out = m()

And then:

scala> val d1: Dim1 = getDimensionTpe(1)
d1: Dim1 = Dim1()

scala> val d2: Dim2 = getDimensionTpe(2)
d2: Dim2 = Dim2()

scala> val d3: Dim3 = getDimensionTpe(3)
d3: Dim3 = Dim3()

If you get the static type wrong, the compiler will tell you:

scala> val d3: Dim1 = getDimensionTpe(3)
<console>:15: error: type mismatch;
 found   : DimMap.dimMap3.Out
    (which expands to)  Dim3
 required: Dim1
       val d3: Dim1 = getDimensionTpe(3)
                                     ^

If you provide an integer literal without a mapping, that's also a compile-time error:

scala> getDimensionTpe(0)
<console>:14: error: could not find implicit value for parameter m: DimMap[shapeless._0]
       getDimensionTpe(0)
                      ^

And finally if you provide an Int argument that isn't an integer literal, you'll also get a compiler error:

scala> val x = 1
x: Int = 1

scala> getDimensionTpe(x)
<console>:16: error: Expression x does not evaluate to a non-negative Int literal
       getDimensionTpe(x)
                       ^

Behind the scenes Shapeless is using a macro to make this possible, and it's not even that scary—you can read the NatMacros definition for details.

Related