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