Search code examples
scalageneric-programmingshapelesshlistarity

In scala shapeless library, is it possible to write a generic arity function when the arity > 22 (presumably using one of shapeless macros)?


The following code is a typical demo of one of shapeless' use case:


  def getHList[P <: Product, F, L <: HList](p: P)(implicit gen: Generic.Aux[P, L]): L = {
    gen.to(p)
  }

    val v = getHList(1, 2, 3, 4, 5, 6, 7, 8, 9)

This gives the proper result, unfortunately, it relies on scala's tuple syntactic suger, and doesn't work when the number of argument > 22:


    val v = getHList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0)

(this generates an error that looks this the follow)

[Error] /xxx/HListSuite.scala:41: 29 more arguments than can be applied to method getHList: (p: P)(implicit gen: shapeless.Generic.Aux[P,L])L
one error found

FAILURE: Build failed with an exception.

I wonder if there is a macro or another scala feature I can use to break this limitation, any advice?

I'm using scala 2.12.8 but can upgrade to 2.13 at any time.


Solution

    • If your goal is to generate an HList longer than 22 then there are plenty of ways

      type _23 = Succ[_22]
      val _23: _23 = new _23
      
      1 :: 2 :: 3 :: 4 :: 5 :: 6 :: 7 :: 8 :: 9 :: 10 :: 11 :: 12 :: 13 :: 14 :: 15 :: 16 :: 17 :: 18 :: 19 :: 20 :: 21 :: 22 :: 23 :: HNil
      import shapeless.syntax.std.traversable._
      import shapeless.ops.hlist.Fill
      (1 to 23).toHList[the.`Fill[_23, Int]`.Out]
      (1 to 23).toSizedHList(_23)
      implicitly[_1 *--* _23].apply //takes long
      

      Be careful, some of these calculations take long.

    • Also you can define Product23, Tuple23

      getHList(Tuple23(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23))
      

      although syntax sugar just with brackets will not work. The name of case class isn't significant, it can be MyClass instead of Tuple23.

    • In Dotty there is TupleXXL.

    • If your goal is to write a method like getHList you can try to make it curried

      //libraryDependencies += "com.github.dmytromitin" %% "auxify-macros" % "0.6", scalacOptions += "-Ymacro-annotations" (in 2.13)
      import com.github.dmytromitin.auxify.macros.{aux, instance}
      
      def curriedGetHList[N <: Nat] = new PartiallyAppliedCurriedGetHList[N]
      
      class PartiallyAppliedCurriedGetHList[N <: Nat] {
        def apply[A](a: A)(implicit cghl: CurriedGetHList[N, A]): cghl.Out = cghl(a)
      }
      
      @aux @instance
      trait CurriedGetHList[N <: Nat, A] {
        type Out
        def apply(a: A): Out
      }
      object CurriedGetHList {
        implicit def mkCurriedGetHList[N <: Nat, A]
        (implicit
         helper: CurriedGetHListHelper[N, A, A :: HNil]
        ): Aux[N, A, helper.Out] = instance(a => helper(a :: HNil))
      }
      
      @aux @instance
      trait CurriedGetHListHelper[N <: Nat, A, L <: HList] {
        type Out
        def apply(acc: L): Out
      }
      object CurriedGetHListHelper {
        implicit def one[A, L <: HList]
        (implicit reverse: Reverse[L]): Aux[_1, A, L, reverse.Out] = instance(acc => reverse(acc))
      
        implicit def succ[N <: Nat, A, L <: HList]
        (implicit helper: Lazy[CurriedGetHListHelper[N, A, A :: L]]): Aux[Succ[N], A, L, A => helper.value.Out] =
          instance(acc => a => helper.value(a :: acc))
      }
      
      curriedGetHList[_10](1).apply(2)(3)(4)(5)(6)(7)(8)(9)(10)
      

      or

      def curriedGetHList[N <: Nat] = new HListBuilder[N, HNil](HNil)
      
      class HListBuilder[N <: Nat, L <: HList](l: L) {
        def apply[A](a: A)(implicit bhl: BuildHList[N, L, A]): bhl.Out = bhl(l, a)
      }
      
      @aux @instance
      trait BuildHList[N <: Nat, L <: HList, A] {
        type Out
        def apply(l: L, a: A): Out
      }
      trait LowPriorityBuildHList {
        implicit def succ[N <: Nat, L <: HList, A]: BuildHList.Aux[Succ[N], L, A, HListBuilder[N, A :: L]] =
          BuildHList.instance((l, a) => new HListBuilder(a :: l))
      }
      object BuildHList extends LowPriorityBuildHList {
        implicit def one[L <: HList, A](implicit reverse: Reverse[A :: L]): Aux[_1, L, A, reverse.Out] =
          instance((l, a) => (a :: l).reverse)
      }
      
      curriedGetHList[_23](1).apply(2).apply(3).apply(4).apply(5).apply(6).apply(7).apply(8).apply(9).apply(10)
        .apply(11).apply(12).apply(13).apply(14).apply(15).apply(16).apply(17).apply(18).apply(19).apply(20)
        .apply(21).apply(22).apply(23)
      
    • Or you can divide long tuple into tuple of tuples and use deep Generic (1 2 3 4).

    • One more option is to write a whitebox macro

      import scala.language.experimental.macros
      import scala.reflect.macros.whitebox
      
      def getHList(xs: Any*): HList = macro getHListImpl
      def getHListImpl(c: whitebox.Context)(xs: c.Tree*): c.Tree = {
        import c.universe._
        xs.foldRight[Tree](q"_root_.shapeless.HNil: _root_.shapeless.HNil")((h, t) => q"_root_.shapeless.::($h, $t)")
      }
      
      getHList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
      

      Since the macro is whitebox its return type will be proper, Int :: Int :: ... :: HNil.

    • Maybe the easiest is to use shapeless.ProductArgs

      def getHList = new ProductArgs {
        def applyProduct[L <: HList](l: L): L = l
      }
      
      getHList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23)
      

      Actually ProductArgs are implemented in Shapeless via scala.Dynamic and whitebox macro.