我经常读到一些编程语言对模块(OCaml,Scala,TypeScript [?])具有“一流”的支持,最近偶然发现了这样的答案,即以模块为一流公民 Scala的显着特征。
我以为我很清楚模块化编程的含义,但是在发生这些事件之后,我开始怀疑我的理解...
我认为模块没什么特别的,只是某些类的实例充当迷你库。小型库代码进入一个类,该类的对象是模块。您可以将它们作为依赖项传递给需要 module 提供的服务的任何其他类,因此,任何体面的OOPL都具有一流的模块,但显然没有!
答案 0 :(得分:6)
模块以及子例程是组织代码的一种方式。在开发程序时,我们将指令打包为子例程,将子例程打包为结构,将结构打包为包,库,程序集,框架,解决方案等。因此,将其他所有内容放在一边,这只是一种组织代码的机制。
我们之所以使用所有这些机制,而不仅仅是线性地布置我们的指令,根本原因是因为程序的复杂度相对于其大小呈非线性增长。换句话说,由n
个片段构建的程序比每个m
指令构建的程序更容易理解。当然,这并不总是正确的(否则,我们可以将程序分为任意部分并感到满意)。实际上,要做到这一点,我们必须引入一种称为 abstraction 的基本机制。仅当每个部分提供某种抽象时,我们才能从将程序拆分为可管理的子部分中受益。例如,我们可以将n*m
,connect_to_database
,query_for_students
和sort_by_grade
抽象打包为函数或子例程,并且更容易理解以这些抽象术语,而不是试图理解内联所有这些函数的代码。
因此,现在我们有了功能,很自然地引入了下一个组织层次-功能集合。常见的是,有些函数围绕某个通用抽象构建族,例如take_the_first_n
,student_name
,student_grade
等,它们都围绕着相同的抽象student_courses
旋转。 student
,connection_establish
等也是如此。因此,我们需要将这些功能结合在一起的某种机制。在这里,我们开始有选择。某些语言采用OOP路径,其中对象和类是组织的单位。一堆函数和一个状态称为对象。其他语言则采取了不同的方法,并决定将功能组合到称为 modules 的静态结构中。主要区别在于模块是静态的编译时结构,其中对象是必须在运行时中创建才能使用的运行时结构。结果,自然地,对象倾向于包含状态,而模块则不包含状态(仅包含代码)。对象本身就是常规值,您可以将其分配给变量,将其存储在文件中,以及进行其他可以处理数据的操作。与对象相反,经典模块没有运行时表示形式,因此您不能将模块作为参数传递给函数,也不能将它们存储在列表中,否则不能对模块执行任何计算。这基本上就是人们所说的“一等公民”的意思,即将实体视为简单值的能力。
返回可组合程序。为了使对象/模块可组合,我们需要确保它们创建了抽象。对于函数,抽象边界已明确定义-它是参数的元组。对于对象,我们有接口和类的概念。而对于模块,我们只有接口。由于模块本来就更简单(它们不包含状态),所以我们不必处理它们的构造和反构造,因此我们不需要 class 的更复杂的概念。类和接口都是通过某种标准对对象和模块进行分类的一种方法,因此我们可以不用考虑实现就可以对不同的模块进行推理,就像使用connection_close
,connect_to_database
等一样。函数-我们仅根据它们的名称和接口(可能还有文档)对它们进行推理。现在,我们可以拥有一个类query_for_students
或一个模块student
来定义一个称为“学生”的抽象,这样我们就可以节省很多脑力,而不必处理这些学生的实现方式。
除了使我们的程序更易于理解之外,抽象还为我们带来了另一个好处-泛化。由于我们不需要推理功能或模块的实现,因此这意味着所有实现在某种程度上都是可以互换的。因此,我们可以编写程序,以便它们以通用的方式表达其行为,而不会破坏抽象,然后在运行程序时选择特定的实例。对象是运行时实例,从本质上讲,它意味着我们可以选择运行时的实现。很好但是,阶级很少是一等公民,因此我们必须发明不同的繁琐方法进行选择,例如Abstract Factory和Builder设计模式。对于模块而言,情况甚至更糟,因为它们本质上是编译时结构,因此我们必须在程序构建/衬砌时选择实现。这不是现代世界中人们想要做的。
这是一流的模块,是模块和对象的融合,它们为我们提供了两个世界的精华-易于推理的无状态结构,而无状态结构又是纯净的一流公民,您可以将其存储在变量中,放入列表中,然后在运行时选择所需的实现。
说到OCaml,在底层,一流的模块只是功能的记录。在OCaml中,您甚至可以将状态添加到一流的模块中,从而几乎无法与对象区分开。这将我们带到了另一个主题-在现实世界中,对象与结构之间的分离还不是很清楚。例如,OCaml既提供模块又提供对象,您可以将对象放入模块内,反之亦然。在C / C ++中,我们具有编译单元,符号可见性,不透明的数据类型和头文件,它们可以进行某种模块化编程,并且具有结构和名称空间。因此,有时很难说出区别。
因此,进行总结。模块是具有明确定义的接口的代码段,用于访问该代码。一流的模块是可以作为常规值进行操作的模块,例如可以存储在数据结构中,分配变量并在运行时选择。
答案 1 :(得分:4)
此处是OCaml的视角。
模块和类非常不同。
首先,OCaml中的类是一个非常特定(且复杂)的功能。为了更详细一些,类实现了继承,行多态性和动态调度(也称为虚拟方法)。它使它们具有高度的灵活性,但却牺牲了一些效率。
模块完全是完全不同的东西。
实际上,您可以将模块视为原子微型库,通常它们用于定义类型及其访问器,但它们的功能远不止于此。
模块通常是静态解决的,因此易于内联,使您可以编写清晰的代码而不必担心效率降低。
现在,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来说,这是一个实际的示例,说明由于实现方面的根本差异,您无法使用类来处理模块:
模块具有函数,可以使用rec
和and
关键字相互递归引用。一个模块还可以使用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
。至少没有更改Base
和Child
来显式启用它的情况:
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
类可以通过常规命名为this
或self
的变量来引用自身(在OCaml中,您可以根据需要命名,但是self
是惯例)。它们也是后期绑定的,这意味着它们可以在运行时解析,因此可以调用定义时不存在的方法实现。这称为开放递归。
那么,为什么模块也不会延迟绑定呢?我认为主要是出于性能方面的考虑。毫无疑问,对每个函数调用的名称进行字典搜索都会对执行时间产生重大影响。