什么是Scala中的一等模块?

11

当说scala通过对象语法提供了一流的模块支持是什么意思?词汇表中没有提到这个短语,但我已经遇到过两次了,而且无法解密。在这篇关于适配器的博客文章中提到了这一点。


4
不,Scala模块系统与许多其他模块系统(例如Java的“packages”或Haskell的模块)的一个不同之处在于模块是一等公民,也就是说你可以将模块作为参数传递、返回它、将它分配给变量、在运行时构建它等。这是因为Scala统一了模块和对象、模块声明和类/特征、模块系统和类型系统。 - Jörg W Mittag
好的,明白了。这样就清楚多了。谢谢大家。 - RoyalCanadianKiltedYaksman
1个回答

16

"模块"是可插拔的软件部件,有时也称为“包(package)"。它通过一组明确定义的接口提供功能,声明了它提供什么和需要什么。最后,它是可互换的。

大多数语言都没有直接支持模块,这主要是因为虽然支持声明API很常见,但不支持声明依赖或需求。在流行语言中,库通常通过依赖于“标准”库提供的类型或要求使用实现其提供的API的对象进行初始化来进行接口连接。

所以,如果我想制作一个基准测试模块,我通常会使用标准库提供的计时工具,或者最糟糕的情况下,我会声明一个计时器类型,并请求在模块的功能可以使用之前用实现该类型的类进行初始化。

然而,当模块支持可用时,我将不仅声明我提供的基准测试接口,而且还将声明我需要一个“时钟模块”——一个导出我所需接口的模块。

我的模块的客户端不需要做任何事情就可以使用我的接口——它可以直接使用。或者它甚至不声明要使用我的基准测试模块,而是声明它对基准测试模块的需求。

什么能够满足需求只有在应用程序级别(模块是应用程序的组件)上才会被确定。在那个点上,它将声明它将使用该客户端、我的基准测试和一个实现我所需时钟的模块。

如果您了解Guice,这可能很熟悉。Guice解决的问题很大程度上是由于Java编程语言中缺乏模块支持造成的。

那么,在Scala中,模块支持是如何工作的呢?嗯,我的模块接口可能看起来像这样:

trait Benchmark extends Clock // What I need {
  // What I provide
  type ABench <: Bench
  trait Bench {
    def measure(task: => Unit): Long
  }
  def aBench: ABench
}

时钟将被定义为一个模块,就像这样:

trait Clock {
  // What it provides
  type AClock <: Clock
  trait Clock {
    def now(): Long
  }
  def aClock: AClock
}

我的模块本身可能看起来像这样:

trait MyModule extends Benchmark {
  class ABench extends Bench {
    def measure(task: => Unit): Long = {
      val measurements = for(_ <- 1 to 10) yield {
        val start = aClock.now()
        task
        val end = aClock.now()
        end - start
      }
      measurements / 10
    }
  }
  object aBench extends ABench
}

时钟模块将被类似地定义。一个应用程序可以声明为由多个模块组合而成:

trait application extends Clock with Benchmark with ...

当然,虽然不需要声明依赖关系,因为已经提供了。你可以将提供应用程序构建要求的模块组合起来使用:

object Application extends MyModule with JavaClock with ...

这将使得MyModule的要求与JavaClock提供的实现相链接。上面的内容仍需要一些共享知识,因为"Clock"很可能是我提供的API。当然,一个人可以编写代理,但这并不是即插即用的。如果我像这样声明我的Benchmark模块,Scala可以更进一步:

trait Benchmark {
  type Clock = {
    def now(): Long
  }
  def aClock: Clock

  // What I provide
  type ABench <: Bench
  trait Bench {
    def measure(task: => Unit): Long
  }
  def aBench: ABench
}

现在,任何提供now(): Long方法的类都可以被用来满足要求,而不需要使用桥接方法。当然,如果方法名称是"millis(): Long"而不是"now(): Long",那么我就无法使用,这种"绑定"是某些支持模块的语言可能会解决的问题,但Scala不会。此外,由于JVM的工作方式,也会存在性能损失。

所以,这就是模块和模块支持。最后是第一流模块(first class module)。对X的第一流支持意味着X可以像值一样被操纵。例如,Scala对函数有第一流支持,这意味着我可以将函数传递给方法,在变量、映射中存储函数等。

对于模块的第一流支持基本上是实例化,虽然可以使用"对象"来创建该模块的单例,然后传递它(我下面进一步讨论了它的优点)。所以,我可以这样做:

object myBenchmark extends MyModule with JVMClock

并将 myBenchmark 作为参数传递给需要此类模块的方法。

在Scala中,有两个元素使所有这些工作:抽象类型和路径相关类型。

抽象类型是“类型”声明,它使代码可以声明将使用类型X,该类型不会由调用或实例化它的人定义,而是在组合模块的时刻定义。

路径相关类型使得可以在不完全不安全的情况下使用模块,但不至于过于限制而不允许任何东西。假设我这样做:

val b: MyModule.Clock = MyModule.aClock

假设我在Benchmark上有一个方法,它需要一个Clock作为参数。我可以在MyModule上调用该方法并将b作为参数传递进去,因为Scala知道b的Clock是绑定到MyModule的。而如果我试图将b传递给实现Benchmark的另一个模块,Scala将不会让我这样做。也就是说,我可以从Benchmark中获取特定于Benchmark抽象类型的值--仅对模块实现者未知--并将其反馈回该Benchmark,但不能反馈回其他Benchmark实现。


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