模块以及子例程是组织代码的一种方式。当我们开发程序时,我们将指令打包成子程序,子程序打包成结构,结构打包成包、库、程序集、框架、解决方案等等。因此,抛开其他一切,它只是一种组织代码的机制。
我们之所以使用所有这些机制,而不是仅仅线性地布置我们的指令,其根本原因是因为程序的复杂性相对于其大小呈非线性增长。换句话说,由n
每个具有指令的片段构建m
的程序比由指令构建的程序更容易理解n*m
。当然,这并不总是正确的(否则我们可以将我们的程序分成任意部分并且很高兴)。事实上,要做到这一点,我们必须引入一种称为抽象的基本机制。只有当每个部分都提供某种抽象时,我们才能从将程序拆分为可管理的子部分中受益。例如,我们可以有, connect_to_database
, query_for_students
, sort_by_grade
, 和take_the_first_n
抽象包装为函数或子例程,并且更容易理解以这些抽象表示的代码,而不是试图理解所有这些函数都内联的代码。
所以现在我们有了函数,很自然地介绍下一个层次的组织——函数的集合。通常可以看到一些函数围绕一些通用抽象构建系列,例如 , ,student_name
等,它们都围绕同一个抽象。,等也是如此。因此我们需要一些将这些功能联系在一起的机制。在这里,我们开始有选择。一些语言采用了 OOP 路径,其中对象和类是组织的单位。一堆函数和一个状态被称为一个对象。其他语言采取了不同的方式,并决定将函数组合成称为模块的静态结构student_grade
student_courses
student
connection_establish
connection_close
. 主要区别在于模块是静态的编译时结构,其中对象是运行时结构,必须在运行时创建才能使用。结果,自然地,对象倾向于包含状态,而模块不包含(并且只包含代码)。对象本质上是常规值,您可以将其分配给变量,将它们存储在文件中,并执行您可以对数据执行的其他操作。与对象相反,经典模块没有运行时表示,因此您不能将模块作为参数传递给函数,将它们存储在列表中,或者对模块执行任何计算。这基本上就是人们所说的一等公民的意思——一种将实体视为简单价值的能力。
回到可组合程序。为了使对象/模块可组合,我们需要确保它们创建抽象。对于函数的抽象边界是明确定义的——它是参数的元组。对于对象,我们有接口和类的概念。而对于模块,我们只有接口。由于模块本质上更简单(它们不包括状态),我们不必处理它们的构造和解构,因此我们不需要更复杂的类概念。类和接口都是一种按照某些标准对对象和模块进行分类的方法,这样我们就可以在不查看实现的情况下推断不同的模块,就像我们对connect_to_database
,query_for_students
, et al 函数 - 我们仅根据它们的名称和接口(可能还有文档)来推理它们。现在我们可以有一个类student
或者一个模块Student
都定义一个叫做student的抽象,这样我们就可以节省很多脑力,而不必处理那些student是如何实现的。
除了让我们的程序更容易理解之外,抽象还给我们带来了另一个好处——泛化. 由于我们不需要对函数或模块的实现进行推理,这意味着所有实现在某种程度上都是可以互换的。因此,我们可以编写我们的程序,让它们以一般的方式表达它们的行为,而不破坏抽象,然后在我们运行程序时选择特定的实例。对象是运行时实例,本质上这意味着我们可以在运行时选择我们的实现。这很好。然而,类很少是一等公民,因此我们必须发明不同的繁琐方法来进行选择,例如抽象工厂和构建器设计模式。对于模块,情况更糟,因为它们本质上是一个编译时结构,我们必须在程序构建/衬里时选择我们的实现。
这里出现了一流的模块,作为模块和对象的融合,它们为我们提供了两个世界中最好的 - 一个易于推理的无状态结构,同时它们是一个纯粹的一等公民,你可以存储在变量中,放入列表并在运行时选择所需的实现。
说到 OCaml,在底层,一流的模块只是函数的记录。在 OCaml 中,您甚至可以将状态添加到第一类模块,使其与对象几乎无法区分。这给我们带来了另一个话题——在现实世界中,对象和结构之间的分离并不那么清楚。例如,OCaml 同时提供模块和对象,您可以将对象放入模块中,反之亦然。在 C/C++ 中,我们有编译单元、符号可见性、不透明数据类型和头文件,它们支持某种模块化编程,还有结构和命名空间。因此,有时很难区分差异。
因此,总结一下。模块是具有明确定义的接口来访问此代码的代码片段。第一类模块是可以作为常规值操作的模块,例如,存储在数据结构中、分配变量并在运行时选取。