一个模拟解析器连接符`~`的shapeless HList提取器

14

问题

是否有可能创建一个类似于以下形式的shapeless HList提取器。

val a ~ _ ~ b = 4 :: "so" :: 4.5 :: HNil
=> a == 4 && b == 4.5
  1. ::替换为~,这应该不是问题。
  2. 摆脱终止的HNil。是否会出现任何问题?

动机

经过多次尝试和努力,我终于成功让以下代码运行:

for(
  x1 :: _ :: x2 :: HNil <- (expInt ~ "+" ~ expInt).llE
) yield (x1 + x2)

expInt 解析了某个单子中的一个 Int(expInt ~ "+" ~ expInt).llE 的类型是 E[Int :: String :: Int :: HNil]

我希望左侧的模式在某种程度上类似于右侧组合解析器的构建。


乍一看,我怀疑提取器语法可能行不通,但这是一个值得追求的目标,祝你好运! - Travis Brown
顺便问一下,我非常想看看你在项目中如何使用shapeless...github链接呢? - Miles Sabin
@Miles 目前情况有些混乱。我已经将正在处理的功能提取出来,形成了一个独立的库,并正在整理中。一旦有机会让其他人能够理解它,我计划将其放在Github上。这将是一个库,用于创建命令行工具(解析器部分)以运行计算实验(单子部分),例如使用不同算法解决不同参数下的问题。我已经将提醒您的事项列入我的待办列表中。 - ziggystar
1个回答

37
这是可以做到的,并且有一些有趣的细节。
第一个是,通常情况下,要匹配使用右结合构造函数(即::)构建的结构,您将使用相应的右结合提取器,否则您将以相反的顺序分解并绑定提取的元素。不幸的是,与右结合运算符一样,右结合提取器必须以Scala中的:结尾,否则提取器名称必须为~:而不是普通的~,这将破坏您的解析组合语法。但是,我将暂时搁置这个问题,并继续使用右结合性。
第二个细节是,我们需要使unapply方法根据我们是否匹配了两个或两个以上元素的HList来产生不同类型的结果(我们根本不能匹配少于两个元素的列表)。
如果我们匹配的是两个以上元素的列表,我们需要将列表分解为由头和HList尾部组成的一对,即给定l:H :: T,其中T <: HList,我们必须生成类型为(H,T)的值。另一方面,如果我们匹配的是正好两个元素的列表,即E1 :: E2 :: HNil的形式,我们需要将列表分解为仅包含这两个元素的一对,即(E1,E2),而不是一个头和一个尾部,后者将是(E1,E2 :: HNil)
这可以使用与shapeless中使用的完全相同的类型级编程技术来完成。首先,我们定义一个类型类,该类型类将执行提取器的工作,并且每个实例都对应于上述两种情况之一。
import shapeless._

trait UnapplyRight[L <: HList] extends DepFn1[L]

trait LPUnapplyRight {
  type Aux[L <: HList, Out0] = UnapplyRight[L] { type Out = Out0 }
  implicit def unapplyHCons[H, T <: HList]: Aux[H :: T, Option[(H, T)]] =
    new UnapplyRight[H :: T] {
      type Out = Option[(H, T)]
      def apply(l: H :: T): Out = Option((l.head, l.tail))
    }
}

object UnapplyRight extends LPUnapplyRight {
  implicit def unapplyPair[H1, H2]: Aux[H1 :: H2 :: HNil, Option[(H1, H2)]] =
    new UnapplyRight[H1 :: H2 :: HNil] {
      type Out = Option[(H1, H2)]
      def apply(l: H1 :: H2 :: HNil): Out = Option((l.head, l.tail.head))
    }
}

然后我们按照如下方式定义与之相关的提取器:
object ~: {
  def unapply[L <: HList, Out](l: L)
    (implicit ua: UnapplyRight.Aux[L, Out]): Out = ua(l)
}

然后我们就可以开始了。

val l = 23 :: "foo" :: true :: HNil

val a ~: b ~: c = l
a : Int
b : String
c : Boolean

到目前为止,一切都很好。现在让我们回到结合性问题。如果我们想使用左结合的提取器(即使用~而不是~:)来实现相同的效果,我们需要改变分解的方式。首先,让我们将刚才使用的右结合提取器语法展开。表达式,

val a ~: b ~: c = l

等价于,
val ~:(a, ~:(b, c)) = l

相比之下,左结合版本为:
val a ~ b ~ c = l

相当于,

val ~(~(a, b), c) = l

为了将其作为HLists的提取器工作,我们的unapply类型类必须从列表末尾而不是开头剥离元素。我们可以借助shapeless的InitLast类型类来实现这一点。
trait UnapplyLeft[L <: HList] extends DepFn1[L]

trait LPUnapplyLeft {
  import ops.hlist.{ Init, Last }
  type Aux[L <: HList, Out0] = UnapplyLeft[L] { type Out = Out0 }
  implicit def unapplyHCons[L <: HList, I <: HList, F]
    (implicit
      init: Init.Aux[L, I],
      last: Last.Aux[L, F]): Aux[L, Option[(I, F)]] =
    new UnapplyLeft[L] {
      type Out = Option[(I, F)]
      def apply(l: L): Out = Option((l.init, l.last))
    }
}

object UnapplyLeft extends LPUnapplyLeft {
  implicit def unapplyPair[H1, H2]: Aux[H1 :: H2 :: HNil, Option[(H1, H2)]] =
    new UnapplyLeft[H1 :: H2 :: HNil] {
      type Out = Option[(H1, H2)]
      def apply(l: H1 :: H2 :: HNil): Out = Option((l.head, l.tail.head))
    }
}

object ~ {
  def unapply[L <: HList, Out](l: L)
    (implicit ua: UnapplyLeft.Aux[L, Out]): Out = ua(l)
}

现在我们完成了,

val a ~ b ~ c = l
a : Int
b : String
c : Boolean

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