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

1

3

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.

Sweepstakes answered 18/1, 2016 at 1:0 Comment(0)
C
3

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).

Cherice answered 18/1, 2016 at 3:21 Comment(2)
Compile-time failure is even better than runtime failure. The idea that the match constraint (only one N choose 2 value type pair matches the (A1, A2)) can be enforced at compile time is very interesting also (in the trust but verify preconditions sense). If you are game to think about this, I'll create a separate, more specific question.Sweepstakes
@Sweepstakes Sure, I think that'd be a great question.Cherice

© 2022 - 2024 — McMap. All rights reserved.