有没有办法扩展类型声明?

Is there a way to extend type declarations?

我正在尝试使用 shapeless 的 Coproduct 进行错误类型聚合。以下是尝试隔离我目前遇到的问题:

import shapeless._

case object F1
case object F2
type F12 = F1.type :+: F2.type :+: CNil

case object F3
case object F4
type F34 = F3.type :+: F4.type :+: CNil

type F1234 = F1.type :+: F2.type :+: F3.type :+: F4.type :+: CNil

def custom(f: Either[F12, F34]): F1234 = // how can I declare the resulting type?
  f.fold(_.extendRightBy[F34], _.extendLeftBy[F12])

object F1234Handler extends Poly1 {
  implicit def caseF1 = at[F1.type](_ => "got F1")
  implicit def caseF2 = at[F2.type](_ => "got F2")
  implicit def caseF3 = at[F3.type](_ => "got F3")
  implicit def caseF4 = at[F4.type](_ => "got F4")
}

custom(Left(Coproduct[F12](F2))).fold(F1234Handler) // got F2

如何在自定义折叠中声明结果类型而不必重复自己?理想情况下,我不想像我那样声明 F1234,我只想通过合并两个现有类型声明 F12 和 F34 来声明它。这样,每当我向这些声明中的任何一个添加另一个故障类型时,我都不需要更新 F1234 声明。我可以声明类型 F1234 = F1.type :+: F2.type :+: F34 但我不能声明类型 F1234 = F12 :+: F34 由于 F12 的 CNil 尾部被丢弃扩展操作。

Scala 依赖类型总是有点奇怪和麻烦。类型级函数被编码为隐含的结果作为类型成员,但就 Scala 编译器而言,它只是一个类型成员。所以你必须这样做:

def custom(f: Either[F12, F34])(implicit er: ExtendRight[F12, F34]): er.Out
  = f.fold(_.extendRightBy[F34], _.extendLeftBy[F12])

不幸的是,这行不通,因为编译器无法判断 extendLeftBy[F12] 的输出是同一类型。我们知道这两种类型总是相同的,但编译器不会,所以我们必须要求一个见证(实际上总是存在)。类似于:

def custom(f: Either[F12, F34])(implicit er: ExtendRight[F12, F34],
  el: ExtendLeft[F34, F12])(implicit w: er.Out =:= el.Out): er.Out
  = f.fold(_.extendRightBy[F34], w(_.extendLeftBy[F12]))

不幸的是,即使这样也行不通,因为我们的类型参数不允许依赖于同一列表中的类型参数,而且我们只能有一个隐式列表。所以我们必须 "lift" 这些类型改为类型参数:

def custom[ERO, ELO](f: Either[F12, F34])(
  implicit er: ExtendRight[F12, F34]{type Out = ERO},
  el: ExtendLeft[F34, F12]{type Out = ELO}, w: ELO =:= ERO): ELO
  = f.fold(_.extendRightBy[F34], w(_.extendLeftBy[F12]))

正如我所说,很麻烦,但应该可以。 (ExtendRightExtendLeftextendRightByextendLeftBy 方法使用的类型 - 任何类似的依赖类型函数可能具有类似的 "helper" 类型)。

情况并不像 lmm 的回答所暗示的那么糟糕,部分原因是 Shapeless 提供了一个 ExtendBy 类型 class 来打包 ExtendLeftByExtendRightBy。所以如果你真的想要 custom 的 return 类型而不是你自己计算并手写出来,你可以使用 ExtendBy:

import shapeless._, ops.coproduct.ExtendBy

case object F1
case object F2
type F12 = F1.type :+: F2.type :+: CNil

case object F3
case object F4
type F34 = F3.type :+: F4.type :+: CNil

def custom(f: Either[F12, F34])(implicit ext: ExtendBy[F12, F34]): ext.Out =
  f.fold(ext.right(_), ext.left(_))

即使您确实需要直接使用 ExtendLeftByExtendRightBy,您也可以让编译器相信它们具有相同的输出类型,使用 Aux 类型和单个共享类型参数。所以不是这个(lmm 代码的完整工作版本):

import ops.coproduct.{ ExtendLeftBy, ExtendRightBy }

def custom[ERO, ELO](f: Either[F12, F34])(implicit
  el: ExtendRightBy[F12, F34] { type Out = ELO },
  er: ExtendLeftBy[F12, F34] { type Out = ERO },
  w: ELO =:= ERO
): ERO = f.fold(l => w(el(l)), er(_))

你可以这样写:

def custom[Out <: Coproduct](f: Either[F12, F34])(implicit
  extL: ExtendRightBy.Aux[F12, F34, Out],
  extR: ExtendLeftBy.Aux[F12, F34, Out]
): Out = f.fold(extL(_), extR(_))

在大多数情况下,如果您静态地知道输入类型,那么您只需自己写出 return 类型并完全跳过隐式参数业务。只有在使用泛型类型时才需要隐式证据,如下所示:

def custom[A <: Coproduct, B <: Coproduct](f: Either[A, B])(implicit
  ext: ExtendBy[A, B]
): ext.Out = f.fold(ext.right(_), ext.left(_))

这适用于任何两个联产品,而不仅仅是 F12F34