8

我经常读到一些编程语言对模块(OCaml、Scala、TypeScript[?])有“一等”支持,最近偶然发现一个关于 SO 的答案,在Scala 的显着特征中引用模块是一等公民。

我以为我非常了解模块化编程的含义,但在这些事件之后,我开始怀疑我的理解......

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

  1. 模块到底是什么?它与普通类或对象有何不同?
  2. (1)与我们都知道的模块化编程有什么关系(或不相关)?
  3. 一门语言拥有一流的模块究竟意味着什么?有什么好处?如果一种语言缺乏这样的特性,有什么缺点?
4

4 回答 4

17

模块以及子例程是组织代码的一种方式。当我们开发程序时,我们将指令打包成子程序,子程序打包成结构,结构打包成包、库、程序集、框架、解决方案等等。因此,抛开其他一切,它只是一种组织代码的机制。

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

所以现在我们有了函数,很自然地介绍下一个层次的组织——函数的集合。通常可以看到一些函数围绕一些通用抽象构建系列,例如 , ,student_name等,它们都围绕同一个抽象。,等也是如此。因此我们需要一些将这些功能联系在一起的机制。在这里,我们开始有选择。一些语言采用了 OOP 路径,其中对象和类是组织的单位。一堆函数和一个状态被称为一个对象。其他语言采取了不同的方式,并决定将函数组合成称为模块的静态结构student_gradestudent_coursesstudentconnection_establishconnection_close. 主要区别在于模块是静态的编译时结构,其中对象是运行时结构,必须在运行时创建才能使用。结果,自然地,对象倾向于包含状态,而模块不包含(并且只包含代码)。对象本质上是常规值,您可以将其分配给变量,将它们存储在文件中,并执行您可以对数据执行的其他操作。与对象相反,经典模块没有运行时表示,因此您不能将模块作为参数传递给函数,将它们存储在列表中,或者对模块执行任何计算。这基本上就是人们所说的一等公民的意思——一种将实体视为简单价值的能力。

回到可组合程序。为了使对象/模块可组合,我们需要确保它们创建抽象。对于函数的抽象边界是明确定义的——它是参数的元组。对于对象,我们有接口和类的概念。而对于模块,我们只有接口。由于模块本质上更简单(它们不包括状态),我们不必处理它们的构造和解构,因此我们不需要更复杂的概念。类和接口都是一种按照某些标准对对象和模块进行分类的方法,这样我们就可以在不查看实现的情况下推断不同的模块,就像我们对connect_to_database,query_for_students, et al 函数 - 我们仅根据它们的名称和接口(可能还有文档)来推理它们。现在我们可以有一个类student或者一个模块Student都定义一个叫做student的抽象,这样我们就可以节省很多脑力,而不必处理那些student是如何实现的。

除了让我们的程序更容易理解之外,抽象还给我们带来了另一个好处——泛化. 由于我们不需要对函数或模块的实现进行推理,这意味着所有实现在某种程度上都是可以互换的。因此,我们可以编写我们的程序,让它们以一般的方式表达它们的行为,而不破坏抽象,然后在我们运行程序时选择特定的实例。对象是运行时实例,本质上这意味着我们可以在运行时选择我们的实现。这很好。然而,类很少是一等公民,因此我们必须发明不同的繁琐方法来进行选择,例如抽象工厂和构建器设计模式。对于模块,情况更糟,因为它们本质上是一个编译时结构,我们必须在程序构建/衬里时选择我们的实现。

这里出现了一流的模块,作为模块和对象的融合,它们为我们提供了两个世界中最好的 - 一个易于推理的无状态结构,同时它们是一个纯粹的一等公民,你可以存储在变量中,放入列表并在运行时选择所需的实现。

说到 OCaml,在底层,一流的模块只是函数的记录。在 OCaml 中,您甚至可以将状态添加到第一类模块,使其与对象几乎无法区分。这给我们带来了另一个话题——在现实世界中,对象和结构之间的分离并不那么清楚。例如,OCaml 同时提供模块和对象,您可以将对象放入模块中,反之亦然。在 C/C++ 中,我们有编译单元、符号可见性、不透明数据类型和头文件,它们支持某种模块化编程,还有结构和命名空间。因此,有时很难区分差异。

因此,总结一下。模块是具有明确定义的接口来访问此代码的代码片段。第一类模块是可以作为常规值操作的模块,例如,存储在数据结构中、分配变量并在运行时选取。

于 2019-06-13T16:25:50.943 回答
9

OCaml 的观点在这里。

模块和类是非常不同的。

首先,OCaml 中的类是一个非常具体(且复杂)的特性。更详细地说,类实现了继承、行多态性和动态调度(也称为虚拟方法)。它允许他们以牺牲一些效率为代价来获得高度的灵活性。

然而,模块是完全不同的东西。

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

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

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

现在,一等公民是可以放入变量中、传递给函数并进行相等性测试的实体。在某种程度上,这意味着它们将被动态评估。

例如,假设您有一个模块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
于 2019-06-13T11:14:40.407 回答
4

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

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

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

于 2019-06-16T18:34:35.423 回答
1

正如已经提到的,“模块”、“类”和“对象”更像是趋势,而不是严格的正式定义。例如,如果您将模块实现为对象,正如我所理解的 Scala 所做的那样,那么显然它们之间没有根本区别,但主要是句法上的区别,使它们在某些用例中更方便。

但是,特别是关于 OCaml,这是一个实际示例,说明由于实现的根本差异,您无法对模块执行某些操作,而您可以对类执行这些操作:

rec模块具有函数,它们可以使用andand关键字递归地相互引用。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

类可以通过通常命名为thisor的变量引用自己self(在 OCaml 中,您可以随意命名,但这self是约定俗成的)。它们也是后期绑定的,这意味着它们在运行时被解析,因此可以调用定义时不存在的方法实现。这称为开放递归。

那么为什么模块也不能后期绑定呢?我认为主要是出于性能原因。对每个函数调用的名称进行字典搜索无疑会对执行时间产生重大影响。

于 2019-06-16T15:18:38.287 回答