Search code examples
generic-programmingshapelesshlist

(Dependantly?) typing containers


Given a type constructor/container F[_] I want to make a combinator which can compose polymorphic types and hlist types into a new container by the following ruleset:

  • F[HNil] and F[HNil] into a F[HNil]
  • F[A] and F[HNil] into a F[A :: HNil]
  • F[HNil] and F[A] into a F[A :: HNil]
  • F[A] and F[B] into a F[A :: B :: HNil]
  • F[L <: HList] and F[R <: HList] into a F[L :: R] (hlist concat)
  • F[A] and F[R <: HList] into a F[A :: R] (hlist prepend)
  • F[L <: HList] and F[B] into a F[L :: B :: HNil] (hlist append)

Is there a nice way to do this in shapeless? Or is there a easier way to solve this particular problem :) The thing is that I have combinators which yield a F[Int], F[HNil] and F[HNil]. Combining these should not yield a F[Int :: HNil :: HNil] (but F[Int :: HNil]) for example. For this purpose now I've made typeclass:

 trait CodecAppender[F, A, B] {
  type Out
  final type CodecOut = Codec[F, Out]
  def apply(a: Codec[F, A], b: Codec[F, B]): CodecOut
}

object CodecAppender {

  type Aux[F, A, B, C] = CodecAppender[F, A, B] { type Out = C }

  private def make[F, A, B, C](decompose: C => (A, B))(implicit S: Monoid[F], D: DecoderAppender.Aux[F, A, B, C]): Aux[F, A, B, C] = new CodecAppender[F, A, B] {
    type Out = C

    def apply(a: Codec[F, A], b: Codec[F, B]): CodecOut = new Codec[F, C] {
      def encode(in: C) = {
        val (head, tail) = decompose(in)
        a.encode(head) |+| b.encode(tail)
      }

      val decoder = D.append(a.decoder, b.decoder)
    }
  }

  implicit def HNilAndHNil[F, O](implicit M: Monoid[F],
                                 D: DecoderAppender.Aux[F, HNil, HNil, HNil]) =
    make[F, HNil, HNil, HNil](r => HNil -> HNil)

  //other cases omitted, but there are 6 more (which I described above)

}

Solution

  • There is a nice way of doing just that using a series of type classes. You can do something if your F[_] has an instance of an Monad:

    trait Conv[A]{
      type Out <: HList
      def apply(a: A): Out
    }
    
    object Conv extends LowPrior{
      implicit def hlist[L <: HList] = new Conv[L]{
        type Out = L
        def apply(hl: L) = hl
      }
    }
    
    trait LowPrior{
      implicit def other[A] = new Conv[A]{
        type Out = H :: HNil
        def apply(a: A) = a :: HNil
      }
    }
    
    def combineF[F[_], A, B](fa: F[A], fb: F[B])(implicit ca: Conv[A], cb: Conv[B], m: Monad[F]) = m.flatMap(fa){ a: A =>
      m.map(fb){ b: B => ca(a) ::: c(b) }
    }
    

    If you want the real return types you could define an Aux type in Conv companion object and then use the Prepend type class of an HList to get the final realized result type. (I'm actually fairly positive you could achieve the same thing with an Applicative or even a Zip type class. Will leave that up to you to discover.)