什么是“一流”模块?

时间:2019-06-13 07:15:36

标签: scala typescript ocaml first-class-modules

我经常读到一些编程语言对模块(OCaml,Scala,TypeScript [?])具有“一流”的支持,最近偶然发现了这样的答案,即以模块为一流公民 Scala的显着特征。

我以为我很清楚模块化编程的含义,但是在发生这些事件之后,我开始怀疑我的理解...

我认为模块没什么特别的,只是某些类的实例充当迷你库。小型库代码进入一个类,该类的对象是模块。您可以将它们作为依赖项传递给需要 module 提供的服务的任何其他类,因此,任何体面的OOPL都具有一流的模块,但显然没有!

  1. 什么是恰好是一个模块?它与普通类或对象有何不同?
  2. (1)与我们都知道的模块化编程有什么关系(或没有)?
  3. 语言具有头等舱模块到底是什么意思?有什么好处?如果一种语言缺乏这种功能,会有什么弊端?

4 个答案:

答案 0 :(得分:6)

模块以及子例程是组织代码的一种方式。在开发程序时,我们将指令打包为子例程,将子例程打包为结构,将结构打包为包,库,程序集,框架,解决方案等。因此,将其他所有内容放在一边,这只是一种组织代码的机制。

我们之所以使用所有这些机制,而不仅仅是线性地布置我们的指令,根本原因是因为程序的复杂度相对于其大小呈非线性增长。换句话说,由n个片段构建的程序比每个m指令构建的程序更容易理解。当然,这并不总是正确的(否则,我们可以将程序分为任意部分并感到满意)。实际上,要做到这一点,我们必须引入一种称为 abstraction 的基本机制。仅当每个部分提供某种抽象时,我们才能从将程序拆分为可管理的子部分中受益。例如,我们可以将n*mconnect_to_databasequery_for_studentssort_by_grade抽象打包为函数或子例程,并且更容易理解以这些抽象术语,而不是试图理解内联所有这些函数的代码。

因此,现在我们有了功能,很自然地引入了下一个组织层次-功能集合。常见的是,有些函数围绕某个通用抽象构建族,例如take_the_first_nstudent_namestudent_grade等,它们都围绕着相同的抽象student_courses旋转。 studentconnection_establish等也是如此。因此,我们需要将这些功能结合在一起的某种机制。在这里,我们开始有选择。某些语言采用OOP路径,其中对象和类是组织的单位。一堆函数和一个状态称为对象。其他语言则采取了不同的方法,并决定将功能组合到称为 modules 的静态结构中。主要区别在于模块是静态的编译时结构,其中对象是必须在运行时中创建才能使用的运行时结构。结果,自然地,对象倾向于包含状态,而模块则不包含状态(仅包含代码)。对象本身就是常规值,您可以将其分配给变量,将其存储在文件中,以及进行其他可以处理数据的操作。与对象相反,经典模块没有运行时表示形式,因此您不能将模块作为参数传递给函数,也不能将它们存储在列表中,否则不能对模块执行任何计算。这基本上就是人们所说的“一等公民”的意思,即将实体视为简单值的能力。

返回可组合程序。为了使对象/模块可组合,我们需要确保它们创建了抽象。对于函数,抽象边界已明确定义-它是参数的元组。对于对象,我们有接口和类的概念。而对于模块,我们只有接口。由于模块本来就更简单(它们不包含状态),所以我们不必处理它们的构造和反构造,因此我们不需要 class 的更复杂的概念。类和接口都是通过某种标准对对象和模块进行分类的一种方法,因此我们可以不用考虑实现就可以对不同的模块进行推理,就像使用connection_closeconnect_to_database等一样。函数-我们仅根据它们的名称和接口(可能还有文档)对它们进行推理。现在,我们可以拥有一个类query_for_students或一个模块student来定义一个称为“学生”的抽象,这样我们就可以节省很多脑力,而不必处理这些学生的实现方式。

除了使我们的程序更易于理解之外,抽象还为我们带来了另一个好处-泛化。由于我们不需要推理功能或模块的实现,因此这意味着所有实现在某种程度上都是可以互换的。因此,我们可以编写程序,以便它们以通用的方式表达其行为,而不会破坏抽象,然后在运行程序时选择特定的实例。对象是运行时实例,从本质上讲,它意味着我们可以选择运行时的实现。很好但是,阶级很少是一等公民,因此我们必须发明不同的繁琐方法进行选择,例如Abstract Factory和Builder设计模式。对于模块而言,情况甚至更糟,因为它们本质上是编译时结构,因此我们必须在程序构建/衬砌时选择实现。这不是现代世界中人们想要做的。

这是一流的模块,是模块和对象的融合,它们为我们提供了两个世界的精华-易于推理的无状态结构,而无状态结构又是纯净的一流公民,您可以将其存储在变量中,放入列表中,然后在运行时选择所需的实现。

说到OCaml,在底层,一流的模块只是功能的记录。在OCaml中,您甚至可以将状态添加到一流的模块中,从而几乎无法与对象区分开。这将我们带到了另一个主题-在现实世界中,对象与结构之间的分离还不是很清楚。例如,OCaml既提供模块又提供对象,您可以将对象放入模块内,反之亦然。在C / C ++中,我们具有编译单元,符号可见性,不透明的数据类型和头文件,它们可以进行某种模块化编程,并且具有结构和名称空间。因此,有时很难说出区别。

因此,进行总结。模块是具有明确定义的接口的代码段,用于访问该代码。一流的模块是可以作为常规值进行操作的模块,例如可以存储在数据结构中,分配变量并在运行时选择。

答案 1 :(得分:4)

此处是OCaml的视角。

模块和类非常不同。

首先,OCaml中的类是一个非常特定(且复杂)的功能。为了更详细一些,类实现了继承,行多态性和动态调度(也称为虚拟方法)。它使它们具有高度的灵活性,但却牺牲了一些效率。

模块完全是完全不同的东西。

实际上,您可以将模块视为原子微型库,通常它们用于定义类型及其访问器,但它们的功能远不止于此。

  • 模块允许您创建几种类型,以及模块类型和子模块。基本上,它们允许创建复杂的分区和抽象。
  • 功能键可为您提供类似于c ++模板的行为。除了安全。基本上,它们是模块上的函数,使您可以在其他模块上参数化数据结构或算法。

模块通常是静态解决的,因此易于内联,使您可以编写清晰的代码而不必担心效率降低。

现在,first-class citizen是一个实体,可以将其放入变量中,然后传递给函数并进行相等性测试。从某种意义上说,这意味着它们将得到动态评估。

例如,假设您有一个模块Jpeg和一个模块Png,它们允许您处理其他类型的图片。静态地,您不知道需要显示哪种图像。因此,您可以使用一流的模块:

let get_img_type filename =
 match Filename.extension filename with
 | ".jpg" | ".jpeg" -> (module Jpeg : IMG_HANDLER)
 | ".png" -> (module Png : IMG_HANDLER)

let display_img img_type filename =
 let module Handler = (val img_type : IMG_HANDLER) in
 Handler.display filename

答案 2 :(得分:3)

模块和对象之间的主要区别通常是

  • 模块是第二类,即它们是相当静态的实体,不能作为值传递,而对象却可以。
  • 模块可以包含类型和所有其他形式的声明(并且类型可以抽象化),而对象通常不能。

但是,您要注意,有些语言可以将模块包装为一等值(例如Ocaml),有些语言可以将对象包含类型(例如Scala)。这使线条有些模糊。在某些类型上仍然存在各种偏见,在类型系统中做出了不同的权衡。例如,对象专注于递归类型,而模块专注于类型抽象并允许任何定义。在没有严重妥协的情况下同时支持这两者是一个非常困难的问题,因为这很快导致类型系统无法确定。

答案 3 :(得分:1)

正如已经提到的,“模块”,“类”和“对象”更像是趋势,而不是严格的形式定义。而且,例如,按照我对Scala的理解,如果将模块作为对象来实现,那么显然它们之间就没有根本的区别,而主要只是语法上的区别,这使得它们在某些使用情况下更加方便。

不过,对于OCaml来说,这是一个实际的示例,说明由于实现方面的根本差异,您无法使用类来处理模块:

模块具有函数,可以使用recand关键字相互递归引用。一个模块还可以使用include“继承”另一个模块的实现并覆盖其定义。例如:

module Base = struct
  let name = "base"
  let print () = print_endline name
end

module Child = struct
  include Base
  let name = "child"
end

但是由于模块是早期绑定的,也就是说,名称是在编译时解析的,所以不可能Base.print而不是Child.name来引用Base.name。至少没有更改BaseChild来显式启用它的情况:

module AbstractBase(T : sig val name : string end) = struct
  let name = T.name
  let print () = print_endline name
end

module Base = struct
  include AbstractBase(struct let name = "base" end)
end

module Child = struct
  include AbstractBase(struct let name = "child" end)
end

另一方面,对于类,覆盖是微不足道的,并且是默认设置:

class base = object(self)
  method name = "base"
  method print = print_endline self#name
end

class child = object
  inherit base
  method! name = "child"
end

类可以通过常规命名为thisself的变量来引用自身(在OCaml中,您可以根据需要命名,但是self是惯例)。它们也是后期绑定的,这意味着它们可以在运行时解析,因此可以调用定义时不存在的方法实现。这称为开放递归。

那么,为什么模块也不会延迟绑定呢?我认为主要是出于性能方面的考虑。毫无疑问,对每个函数调用的名称进行字典搜索都会对执行时间产生重大影响。