Search code examples
scalafunctional-programmingshapelesshlist

Invoke a Scala Function2 with a shapeless HList whose values do not match the argument order


I'd like to build the equivalent of:

def applyWithHList2[A1, A2, R, L <: HList](l: L, f: Function2[A1, A2, R]): Try[R]

The values in the list are such that in the N choose 2 possible value combinations of l.unify there is at most one that could be used to call the function. No additional type information is available.

If there is no way to call the function, the result should be Failure with MatchError. Otherwise, the result should be Try(f(a1, a2)).

I am still getting used to shapeless and would appreciate suggestions for how to approach this problem.


Solution

  • Funnily enough it's a lot easier to write a version that just doesn't compile if appropriately typed elements aren't available in the HList:

    import shapeless._, ops.hlist.Selector
    
    def applyWithHList2[A1, A2, R, L <: HList](l: L, f: (A1, A2) => R)(implicit
      selA1: Selector[L, A1],
      selA2: Selector[L, A2]
    ): R = f(selA1(l), selA2(l))
    

    If you really want a runtime error (in a Try) for cases where there's not an applicable pair, you could use the default null instance trick:

    import scala.util.{ Failure, Success, Try }
    
    def applyWithHList2[A1, A2, R, L <: HList](l: L, f: (A1, A2) => R)(implicit
      selA1: Selector[L, A1] = null,
      selA2: Selector[L, A2] = null
    ): Try[R] = Option(selA1).flatMap(s1 =>
      Option(selA2).map(s2 => f(s1(l), s2(l)))
    ).fold[Try[R]](Failure(new MatchError()))(Success(_))
    

    If you find that unpleasant (and it is), you could use a custom type class:

    trait MaybeSelect2[L <: HList, A, B] {
      def apply(l: L): Try[(A, B)] = (
        for { a <- maybeA(l); b <- maybeB(l) } yield (a, b)
      ).fold[Try[(A, B)]](Failure(new MatchError()))(Success(_))
    
      def maybeA(l: L): Option[A]
      def maybeB(l: L): Option[B]
    }
    
    object MaybeSelect2 extends LowPriorityMaybeSelect2 {
      implicit def hnilMaybeSelect[A, B]: MaybeSelect2[HNil, A, B] =
        new MaybeSelect2[HNil, A, B] {
          def maybeA(l: HNil): Option[A] = None
          def maybeB(l: HNil): Option[B] = None
        }
    
      implicit def hconsMaybeSelect0[H, T <: HList, A](implicit
        tms: MaybeSelect2[T, A, H]
      ): MaybeSelect2[H :: T, A, H] = new MaybeSelect2[H :: T, A, H] {
        def maybeA(l: H :: T): Option[A] = tms.maybeA(l.tail)
        def maybeB(l: H :: T): Option[H] = Some(l.head)
      }
    
      implicit def hconsMaybeSelect1[H, T <: HList, B](implicit
        tms: MaybeSelect2[T, H, B]
      ): MaybeSelect2[H :: T, H, B] = new MaybeSelect2[H :: T, H, B] {
        def maybeA(l: H :: T): Option[H] = Some(l.head)
        def maybeB(l: H :: T): Option[B] = tms.maybeB(l.tail)
      }
    }
    
    trait LowPriorityMaybeSelect2 {
      implicit def hconsMaybeSelect2[H, T <: HList, A, B](implicit
        tms: MaybeSelect2[T, A, B]
      ): MaybeSelect2[H :: T, A, B] = new MaybeSelect2[H :: T, A, B] {
        def maybeA(l: H :: T): Option[A] = tms.maybeA(l.tail)
        def maybeB(l: H :: T): Option[B] = tms.maybeB(l.tail)
      }
    }
    

    And then:

    def applyWithHList2[A1, A2, R, L <: HList](l: L, f: (A1, A2) => R)(implicit
      ms2: MaybeSelect2[L, A1, A2]
    ): Try[R] = ms2(l).map(Function.tupled(f))
    

    But that's a lot of work just to throw away some compile-time safety.

    Note that none of these approaches enforce the constraint that there's only at most pair of elements in the HList that the function can be applied to, since I read that as a pre-condition in your question. It'd definitely be possible to write a solution that enforced the constraint at compile time (and it might even be a bit shorter than the MaybeSelect2 implementation above).