scala 什么是“头等舱”模块?

0dxa2lsx  于 12个月前  发布在  Scala
关注(0)|答案(4)|浏览(156)

我经常读到一些编程语言对模块有“一流”的支持(OCaml、Scala、TypeScript[?]),最近我偶然发现了一个关于SO的答案,在Scala的显著特性中,它引用了 * 模块作为一流公民 *。
我以为我很清楚什么是模块化编程,但在这些事件之后,我开始怀疑我的理解...
我认为模块并没有什么特别的,只是作为迷你库的某些类的示例。迷你库的代码进入一个类,该类的对象就是模块。你可以把它们作为依赖传递给任何其他需要 module 提供的服务的类,所以任何像样的OOPL都有一流的模块,但显然没有!
1.模块究竟是什么?它与普通的类或对象有什么不同?
1.(1)与我们都知道的模块化编程有什么关系?
1.对于一种语言来说,拥有一级模块到底意味着什么?它有什么好处?如果一种语言没有这样的特性,它有什么缺点?

qq24tv8q

qq24tv8q1#

模块和子例程一样,都是一种组织代码的方式。当我们开发程序时,我们将指令打包成子例程,将子例程打包成结构,将结构打包成包、库、程序集、框架、解决方案等等。所以,抛开其他一切不谈,模块只是一种组织代码的机制。
我们之所以使用所有这些机制,而不只是线性地排列指令,其根本原因是因为程序的复杂性与其大小成非线性增长。换句话说,由n个片段(每个片段包含m条指令)构建的程序比由n*m条指令构建的程序更容易理解。当然,这是不总是正确(否则,我们可以将程序拆分为任意部分,然后就很高兴了)。事实上,要实现这一点,我们必须引入一种称为 abstraction 的基本机制。只有当每个部分都提供某种抽象时,我们才能从将程序拆分为可管理的子部分中获益。例如,我们可以有connect_to_databasequery_for_studentssort_by_gradetake_the_first_n抽象打包为函数或子例程,并且更容易理解根据这些抽象表达的代码,而不是试图理解其中所有这些函数都是内联的代码。
因此,现在我们有了函数,很自然地要引入下一个层次的组织--函数集合。常见的是,一些函数围绕一些公共的抽象构建家族,例如,student_namestudent_gradestudent_courses等,它们都围绕相同的抽象student。因此,我们需要一些机制来将这些函数联系在一起。现在我们开始有选择了。一些语言选择了OOP路径,其中对象和类是组织的单位。一组函数和一个状态被称为对象。其他语言则采取了不同的方式,决定将函数组合成称为“模块”的静态结构。。主要的区别在于模块是静态的编译时结构,其中对象是运行时结构,必须在运行时创建才能使用。因此,很自然地,对象倾向于包含状态,而模块则不包含状态(并且只包含代码)。而对象本身就是正则值,您可以将其分配给变量,将其存储在文件中,并可以对数据进行其他操作。经典的模块,与对象相反,没有运行时表示,因此不能将模块作为参数传递给函数,不能将它们存储在列表中,也不能对模块执行任何计算。这基本上就是人们所说的“头等公民”的意思--将实体作为简单值处理的能力。
回到可组合程序。为了使对象/模块可组合,我们需要确保它们创建抽象。对于函数,抽象边界被清楚地定义-它是参数的元组。对于对象,我们有接口和类的概念。2而对于模块我们只有接口。3因为模块本质上更简单(不包括国家)我们不必去处理它们的建构和解构,因此我们不需要一个更复杂的类的概念类和接口都是一种通过某种标准对对象和模块进行分类的方法,这样我们就可以在不查看实现的情况下对不同的模块进行推理,就像我们对connect_to_databasequery_for_students等函数所做的那样-我们只是根据它们的名称和接口来推理现在我们可以有一个类student或一个模块Student,它们都定义了一个称为student的抽象,这样我们就可以保存大量的脑力,而不必处理这些学生如何实现的方式。
除了使我们的程序更容易理解之外,抽象还给我们带来了另一个好处-- * 泛化 *。由于我们不需要对函数或模块的实现进行推理,这意味着所有的实现在某种程度上是可互换的。因此,我们可以编写程序,使它们以一种通用的方式表达它们的行为,而不破坏抽象。然后当我们运行程序时选择特定的示例。对象是运行时示例,本质上这意味着我们可以在运行时选择我们的实现。这很好。然而,类很少是一等公民,因此我们不得不发明不同的繁琐方法来进行选择。我喜欢抽象工厂和构建器设计模式。对于模块,情况更糟,因为它们本质上是一个编译时结构,我们必须在程序构建/排队时选择我们的实现。这不是现代世界中人们想要做的。

第一类模块是模块和对象的结合,它们给予了我们两个世界中最好的一个--一个容易理解的无状态结构,同时也是一个纯粹的第一类公民,你可以把它存储在一个变量中,放入列表中,并在运行时选择所需的实现。

**说到OCaml,在其背后,一级模块只是函数的记录。**在OCaml中,您甚至可以向一级模块添加状态,使其与对象几乎无法区分。这将我们带到另一个主题-在真实的世界中,对象和结构之间的分离并不那么清楚。例如,OCaml提供了模块和对象,你可以把对象放在模块中,甚至反之亦然。在C/C++中,我们有编译单元,符号可见性,不透明数据类型和头文件,这使得某种模块化编程成为可能,我们也有结构和名称空间。因此,有时很难区分。

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

wsewodh2

wsewodh22#

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

字符串

r8uurelv

r8uurelv3#

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

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

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

k2arahey

k2arahey4#

如前所述,“模块”、“类”和“对象”更像是一种趋势,而不是严格的形式定义。如果你把模块实现为对象,比如我理解的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是惯例)。它们也是后期绑定的,这意味着它们在运行时被解析,因此可以调用定义时不存在的方法实现。这称为开放递归。
那么为什么模块不也进行后期绑定呢?我认为这是出于性能原因。对每个函数调用的名称进行字典搜索无疑会对执行时间产生重大影响。

相关问题