【问题标题】:Scala: type-based list partitioningScala:基于类型的列表分区
【发布时间】:2014-10-03 00:55:04
【问题描述】:

我有这段代码想要改进:

sealed abstract class A
case class B() extends A
case class C() extends A
case class D() extends A

case class Foo[+T <: A](a: T)

/** Puts instances that match Foo(B()) in the first list and everything else,
  * i.e. Foo(C()) and Foo(D()), in the second list. */
def partition(foos: List[Foo[_ <: A]]): (List[Foo[B]], List[Foo[_ <: A]]) = {
  // ...
}

我想在以下方面对此进行改进:

  1. 我能否更改partition 的返回类型,使其表明第二个列表中没有Foo[B]
  2. 我可以去掉Foo 的类型参数T(即将Foo 更改为case class Foo(a: A))并仍然用相同的类型保证声明partition 吗? (显然,它必须返回不同于 (List[Foo], List[Foo]) 的内容。)

P.S.:如果“shapeless”标签与这个问题无关,请告诉我。

【问题讨论】:

    标签: scala types shapeless


    【解决方案1】:

    这个问题有点棘手,因为 Scala 混合了 algebraic data types(就像你的 A)和子类型。在大多数带有 ADT 的语言中,BCD 根本不是类型——它们只是“构造函数”(在某种意义上与 OOP 构造函数相似但不同)。

    在这些语言(如 Haskell 或 OCaml)中谈论 Foo[B] 是没有意义的,但在 Scala 中您可以,因为 Scala 将 ADT 实现为扩展基本特征或类的案例类(和类对象) .不过,这并不意味着您应该到处谈论Foo[B],一般来说,如果您想用 FP 术语思考并利用类型系统来发挥自己的优势,那么最好不要.

    回答您的具体问题:

    1. 不,没有任何方便的方式。您可以使用标记联合(带有Either[Foo[C], Foo[D]] 元素的列表)或类似Shapeless 的Coproduct(带有Foo[C] :+: Foo[D] :+: CNil 元素的列表)来表示“A 类型的事物列表,但不是B” ,但是这两种方法都是相当笨重的机器,可能一开始就不是最好的主意。
    2. 我建议不要在 A 的子类型上参数化 Foo,但如果您希望能够在类型级别表示“包含 BFoo”,您将需要保持当前的方法。

    为了解决您的后记:如果您想在 ADT 上进行泛化,Shapeless 绝对适用 - 例如,请参阅我的博客文章 here 关于构造函数分区。不过,如果你只是为A 这样做,Shapeless 可能不会给你买太多。


    作为脚注,如果我真的需要一个分区操作来拆分Foo[B] 类型的元素,我可能会这样写:

    def partition(foos: List[Foo[A]]): (List[Foo[B]], List[Foo[A]]) =
      foos.foldRight((List.empty[Foo[B]], List.empty[Foo[A]])) {
        case (Foo(B()), (bs, others)) => (Foo(B()) :: bs, others)
        case (other, (bs, others)) => (bs, other :: others)
      }
    

    这并不理想——如果我们真的想要一个List[Foo[B]],最好有一个List[Foo[~B]] 来代表剩菜——但也不算太糟糕。

    【讨论】:

    • 感谢您的回答。是的,我不喜欢用A 的子类型参数化Foo,因为大多数时候我只会使用Foo[A],与非参数化Foo 相比,它不会传达任何额外的信息。我的分区函数至少可以返回(List[(Foo, B)], List[Foo]),这样我就不用重复B上的模式匹配了。
    • 如果我将我的A 更改为type A = B :+: C :+: D :+: CNil,最好让B :-: A 从副产品A 中删除B。我想这是不可能实现的?
    • 您可以通过类型类获得类型为B :-: A 的证据,但我认为这种语法是不可能的。
    • 你的意思是一个类型类,比如AMinusB,它类似于Ionuț 的回答中的特征T(或NotB)?
    • 好吧,类型类至少具有可以临时建立类型类成员资格的优点。
    【解决方案2】:

    我要展示的不是一个非常灵活的解决方案。它的用途完全取决于您要建模的内容,但我认为这仍然是一种有趣的方法。

    您可以在层次结构中引入第二个特征,例如 T,并从中扩展所有非 B 案例类。

    sealed trait A
    sealed trait T extends A
    
    case class B() extends A
    case class C() extends A with T
    case class D() extends A with T
    
    case class Foo[+T <: A](a: A)
    
    def partition(foos: List[Foo[A]]): (List[Foo[B]], List[Foo[T]]) = ???
    

    【讨论】:

    • 感谢您的回答。但是,它不能很好地扩展。让我们将您的 T 重命名为 NotB。然后应该有NotCNotD,和B() extends A with NotC with NotDC() extends A with NotB with NotDD() extends A with NotB with NotC。样板文件随着类型数量的平方而增长,但它没有传达任何我的原始代码中尚不存在的新信息。
    • @TomasMikula 是的,我同意缩放问题。这就是为什么我在答案的乞求中添加了注释。我不同意与您的原始代码相比,它没有提供任何额外的信息。 partition 的返回类型表示第二个元组元素不会包含 B 的实例。
    • 对。我的意思是 A 及其子类的新定义(扩展 Not* 特征)没有传达任何新信息。
    • 无论如何,A 和子类定义在与partition 函数不同的位置(实际上是不同的库)。我希望定义partition 所需的所有额外信息与partition 本身位于同一位置。
    • @TomasMikula 您可能会发现这个答案(和线程)很有用。 groups.google.com/d/msg/scala-user/8YpX1VkIkDs/dxwBV5m_drEJ 这与 Travis Brown 建议的 typeclass 类似,只是它是通过使用宏来自动化的。
    猜你喜欢
    • 1970-01-01
    • 2011-04-04
    • 1970-01-01
    • 2017-01-11
    • 2020-05-02
    • 1970-01-01
    • 2021-03-18
    • 1970-01-01
    • 2016-01-01
    相关资源
    最近更新 更多