【问题标题】:Generic transform/fold/map over tuple/hlist containing some F[_]包含一些 F[_] 的元组/hlist 上的通用变换/折叠/映射
【发布时间】:2014-10-21 13:03:02
【问题描述】:

我最近问过Map and reduce/fold over HList of scalaz.Validation,并得到了一个很好的答案,关于如何将固定大小的Va[T]scalaz.Validation[String, T] 的别名)元组转换为scalaz.ValidationNel[String, T]。从那时起,我一直在研究 Shapeless 和类型级编程,以尝试提出一种适用于任何大小的元组的解决方案。

这是我开始的:

import scalaz._, Scalaz._, shapeless._, contrib.scalaz._, syntax.std.tuple._

type Va[A] = Validation[String, A]

// only works on pairs of Va[_]
def validate[Ret, In1, In2](params: (Va[In1], Va[In2]))(fn: (In1, In2) => Ret) = {
  object toValidationNel extends Poly1 {
    implicit def apply[T] = at[Va[T]](_.toValidationNel)
  }
  traverse(params.productElements)(toValidationNel).map(_.tupled).map(fn.tupled)
}

那么validate 是我这样称呼的助手:

val params = (
  postal  |> nonEmpty[String]("no postal"),
  country |> nonEmpty[String]("no country") >=> isIso2Country("invalid country")
)

validate(params) { (postal, country) => ... }

我开始使用任何Product 而不是一对并将其内容限制为Va[T]

// needs to work with a tuple of Va[_] of arbitrary size
def validateGen[P <: Product, F, L <: HList, R](params: P)(block: F)(
  implicit
  gen: Generic.Aux[P, L],
  va:  UnaryTCConstraint[L, Va],
  fp:  FnToProduct.Aux[F, L => R]
) = ???

我确实觉得简单地添加约束只能确保输入有效,但对实现函数体没有任何帮助,但我不知道如何去纠正它。

traverse 然后开始抱怨缺少证据,所以我最终得到:

def validateGen[P <: Product, F, L <: HList, R](params: P)(block: F)(
  implicit
  gen: Generic.Aux[P, L],
  va:  UnaryTCConstraint[L, Va],
  tr:  Traverser[L, toValidationNel.type],
  fp:  FnToProduct.Aux[F, L => R]
) = {
  traverse(gen.to(params): HList)(toValidationNel).map(_.tupled).map(block.toProduct)
}

然而,编译器继续抱怨缺少 Traverser[HList, toValidationNel.type] 隐式参数,即使它存在。

我需要向编译器提供哪些额外证据才能编译traverse 调用?是否与UnaryTCConstraint 没有以对traverse 调用有用的方式声明有关,即它不能将toValidationNel 应用于params,因为它不能证明params 仅包含Va[_] ?

P.S. 我还找到了leftReduce Shapeless HList of generic types 并尝试使用foldRight 而不是traverse 无济于事;在尝试诊断编译器真正缺乏哪些证据时,错误消息并没有太大帮助。

更新:

根据 lmm 所指出的,我已将演员表移至 HList,但是,问题出在现在,而在非通用解决方案中,我可以在 traverse 的结果上调用 .map(_.tupled).map(block.toProduct)打电话,我现在得到:

值图不是 shapeless.contrib.scalaz.Out 的成员

怎么可能是traverse(params.productElements)(toValidationNel) 调用的结果而不是通用遍历?

更新 2:

Traverser[...] 位更改为Traverser.Aux[..., Va[L]] 有助于编译器确定遍历的预期结果类型,但是,这只会使validateGen 函数编译成功,但会在调用站点产生另一个错误:

[error] could not find implicit value for parameter tr: shapeless.contrib.scalaz.Traverser.Aux[L,toValidationNel.type,Va[L]]
[error]     validateGen(params) { (x: String :: String :: HNil) => 3 }
[error]                         ^

我也觉得UnaryTCConstraint 完全没有必要——但我对 Shapeless 还是太陌生了,不知道是不是这样。

更新 3:

意识到从遍历器出来的类型不能是Va[L],因为L本身已经是Va[_]的一个hlist,我已经将L类型参数拆分为InOut

def validateGen[P <: Product, F, In <: HList, Out <: HList, R](params: P)(block: F)(
  implicit
  gen: Generic.Aux[P, In],
  va:  UnaryTCConstraint[In, Va],  // just for clarity
  tr:  Traverser.Aux[In, toValidationNel.type, Va[Out]],
  fn:  FnToProduct.Aux[F, Out => R]
): Va[R] = {
  traverse(gen.to(params))(toValidationNel).map(block.toProduct)
}

这编译得很好——我很想知道以前的版本是如何以Va[L] 作为返回值(即Traverser.Aux 的第三个参数)甚至编译的——但是,在调用站点上,我现在得到:

未指定值参数tr,fn

【问题讨论】:

    标签: scala scalaz shapeless scalaz7 type-level-computation


    【解决方案1】:

    您有一个Traverser[L, toValidationNel.type],它与Traverser[HList, toValidationNel.type] 不同(它必须适用于任何HList - 没有机会)。我不知道你为什么写gen.to(params): HList,但这是在丢弃类型信息;那不应该是L的类型吗?

    这可能只会让问题更上一层楼;我怀疑您能否自动获得所需的Traverser。但是您应该能够编写一个隐式方法来提供基于 UnaryTCConstraint 的方法,并且 shapeless 可能已经包含了该方法,并且可以正常工作。

    更新:

    在第一个示例中,编译器知道它正在使用的特定Traverser 实例,因此它知道Out 类型是什么。在validateGen 中,您没有对tr.Out 进行任何约束,因此编译器无法知道它是支持.map 的类型。如果您知道遍历的输出需要是什么,那么您可能需要一个适当的Traverser.Aux,即:

    tr: Traverser.Aux[L, toValidationNel.type, Va[L]]
    

    (只是不要问我如何确保类型推断仍然有效)。

    我想你可能想要.map(_.tupled),因为_已经有一个HList(我怀疑它在原来的validate中也是多余的),但是我以前从未使用过.toProduct,所以也许你是对的。

    更新 2:

    没错,这正是我最初所怀疑的。看看Sequencer 的实现,我怀疑你是对的,UnaryTCConstraint 将被Traverser 包含。如果你不使用它,那么不需要它。

    我能给出的唯一建议是追查那些应该提供你暗示的电话。例如。 Traverser 应该来自 Traverser.mkTraverser。因此,如果您尝试调用Traverser.mkTraverser[String :: String :: HNil, toValidationNel.type, Va[String] :: Va[String] :: HNil],那么您应该能够看到是找不到 Mapper 还是 Sequencer。然后,您可以通过应该发生的隐式调用进行递归,直到找到一个更简单的情况,该情况应该可以正常工作,但不能正常工作。

    【讨论】:

    • 我真的很愚蠢,将这个转换保留为HList — 删除它并且遍历开始正常工作,但是,现在对返回值的.map() 调用无法编译;我已经相应地更新了问题。
    • 谢谢!实际上,我通过查看和源代码自己找出了Traverser.Aux;对于原始validate中的.tupled,它将HList转换为元组,以便可以将其传递给block.tupled。无论如何,函数本身现在可以编译,但在调用站点上找不到 Traverser.Aux 的实例...
    【解决方案2】:

    经过长时间的实验、挫折和死脑细胞,我从零开始,没有Traverser,而是选择了MapperSequencer;我稍后会尝试看看我是否可以再次使用Traverser(如果不是为了实用,至少是为了学习目的):

    def validate[P <: Product, F, L1 <: HList, L2 <: HList, L3 <: HList, R](params: P)(block: F)(
      implicit
      gen: Generic.Aux[P, L1],
      mp:  Mapper.Aux[toValidationNel.type, L1, L2],
      seq: Sequencer.Aux[L2, VaNel[L3]],
      fn:  FnToProduct.Aux[F, L3 => R]
    ): VaNel[R] = {
      sequence(gen.to(params).map(toValidationNel)).map(block.toProduct)
    }
    

    这是证明——双关语——它运行http://www.scastie.org/7086

    【讨论】:

    • 如果您有Mapper.Aux[toValidationNel.type, L1, L2]Sequencer.Aux[L2, VaNel[L3]],那么您应该能够获得Traverser.Aux[L1, toValidationNel.type, VaNel[L3]]。我很惊讶 VaNel[L3] 是可推断的 - 您可能需要制作另一种类型并隐含见证它与 FnToProduct.Aux 中的类型相同。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-29
    • 2023-03-04
    • 2020-01-01
    • 1970-01-01
    相关资源
    最近更新 更多