什么是“一流”模块?

Ash*_*ary 5 ocaml scala typescript first-class-modules

我经常阅读一些编程语言对模块的支持(“一流”)(OCaml,Scala,TypeScript [?]),最近偶然发现了这样的答案,因此将模块引用 Scala的显着特征之一就是一流的公民

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

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

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

Pat*_*atJ 8

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
Run Code Online (Sandbox Code Playgroud)


ivg*_*ivg 7

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

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

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

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

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

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

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

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