从两个HList中创建所有配对的HList

14

我在Scala中使用shapeless,希望编写一个函数allPairs,该函数将获取两个HList并返回所有元素对的HList。例如:

import shapeless._
val list1 = 1 :: "one" :: HNil
val list2 = 2 :: "two" :: HNil
// Has value (1, 2) :: (1, "two") :: ("one", 2) :: ("one", "two") :: HNil
val list3 = allPairs(list1, list2)

有什么想法如何做到这一点吗?

此外,我想强调我正在寻找一个函数,而不是一个内联的代码块。


你认为“函数”和“内联代码块”有什么区别? - Randall Schulz
1
@RandallSchulz:看一下Travis在下面的第一个答案和他使用liftA2的最终答案之间的区别。 - emchristiansen
1个回答

17

你不能在这里使用列表推导式formapflatMap的组合(正如其他答案所建议的那样),因为HList上的这些方法需要高阶函数。如果你只有两个静态类型的列表,这很容易:

import shapeless._

val xs = 1 :: 'b :: 'c' :: HNil
val ys = 4.0 :: "e" :: HNil

object eachFirst extends Poly1 {
  implicit def default[A] = at[A] { a =>
    object second extends Poly1 { implicit def default[B] = at[B](a -> _) }
    ys map second
  }
}

val cartesianProductXsYs = xs flatMap eachFirst

以下是(适当类型化的)结果:

这给我们带来了以下结果:

(1,4.0) :: (1,e) :: ('b,4.0) :: ('b,e) :: (c,4.0) :: (c,e) :: HNil

编写一个使用HList参数执行此操作的方法比较棘手。这里有一个快速示例,展示如何使用一些稍微通用的机制实现它。

首先需要指出的是,我们可以将两个普通列表的笛卡尔积视为将一个接受两个参数并将它们作为元组返回的函数“提升”到列表的可应用函子中。例如,您可以在Haskell中编写以下代码

import Control.Applicative (liftA2)

cartesianProd :: [a] -> [b] -> [(a, b)]
cartesianProd = liftA2 (,)

我们可以在这里编写一个与(,)对应的多态二元函数:

import shapeless._

object tuple extends Poly2 {
  implicit def whatever[A, B] = at[A, B] { case (a, b) => (a, b) }
}

为了完整起见,我们再次定义示例列表:

val xs = 1 :: 'b :: 'c' :: HNil
val ys = 4.0 :: "e" :: HNil

现在,我们将着手编写一个名为liftA2的方法,使我们能够编写如下代码:

liftA2(tuple)(xs, ys)

获取正确结果。名称liftA2有点误导性,因为我们实际上没有一个适用的函子实例,并且它不是通用的。我正在使用HList上命名为flatMapmap的方法模型,并且接受更好建议。

现在我们需要一个类型类,允许我们获取Poly2,对其进行部分应用,并将生成的一元函数映射到HList上:

trait ApplyMapper[HF, A, X <: HList, Out <: HList] {
  def apply(a: A, x: X): Out
}

object ApplyMapper {
  implicit def hnil[HF, A] = new ApplyMapper[HF, A, HNil, HNil] {
    def apply(a: A, x: HNil) = HNil
  }
  implicit def hlist[HF, A, XH, XT <: HList, OutH, OutT <: HList](implicit
    pb: Poly.Pullback2Aux[HF, A, XH, OutH],
    am: ApplyMapper[HF, A, XT, OutT]
  ) = new ApplyMapper[HF, A, XH :: XT, OutH :: OutT] {
    def apply(a: A, x: XH :: XT) = pb(a, x.head) :: am(a, x.tail)
  }
}

现在有一个类型类可用于帮助提升:

trait LiftA2[HF, X <: HList, Y <: HList, Out <: HList] {
  def apply(x: X, y: Y): Out
}

object LiftA2 {
  implicit def hnil[HF, Y <: HList] = new LiftA2[HF, HNil, Y, HNil] {
    def apply(x: HNil, y: Y) = HNil
  }

  implicit def hlist[
    HF, XH, XT <: HList, Y <: HList,
    Out1 <: HList, Out2 <: HList, Out <: HList
  ](implicit
    am: ApplyMapper[HF, XH, Y, Out1],
    lift: LiftA2[HF, XT, Y, Out2],
    prepend : PrependAux[Out1, Out2, Out]
  ) = new LiftA2[HF, XH :: XT, Y, Out] {
    def apply(x: XH :: XT, y: Y) = prepend(am(x.head, y), lift(x.tail, y))
  }
}

最后是我们的方法本身:

def liftA2[HF, X <: HList, Y <: HList, Out <: HList](hf: HF)(x: X, y: Y)(implicit
  lift: LiftA2[HF, X, Y, Out]
) = lift(x, y)

现在,liftA2(tuple)(xs, ys) 就是这样了。

scala> type Result =
     |   (Int, Double) :: (Int, String) ::
     |   (Symbol, Double) :: (Symbol, String) ::
     |   (Char, Double) :: (Char, String) :: HNil
defined type alias Result

scala> val res: Result = liftA2(tuple)(xs, ys)
res: Result = (1,4.0) :: (1,e) :: ('b,4.0) :: ('b,e) :: (c,4.0) :: (c,e) :: HNil

就像我们想要的那样。


2
这很好。你能展示一下如何将它打包成一个以xs和ys为参数的方法吗?我尝试过这种方式,但失败了。 - Régis Jean-Gilles
@emchristiansen:对的-请参见此推文和Miles Sabin的回复。我今天早上出门时开始着手处理它,如果没有其他人先完成,我将在今晚更新。 - Travis Brown
@TravisBrown:令人印象深刻。谢谢。 - emchristiansen
2
干得好。我希望我能再点一次赞。不过,我也不确定是否应该为看到结果有多么复杂而感到兴奋。我猜这就是元编程的乐趣和痛苦吧。顺便问一下,shapeless 有没有文档?尝试解决这个难题时的部分困难在于反向工程 Pullback1Aux、Case1、Case2 和所有生活在 shapeless 动物园里的有趣的动物的目的。 - Régis Jean-Gilles
如果你不需要通用版本,就可以在顶部删掉几行代码;否则,你可能需要清理一下代码。但是这基本上是对类型系统的滥用,所以不太可能变得更加简洁。 - Travis Brown
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接