纯功能语言如何处理模块化?


23

我来自于面向对象的背景知识,在该背景知识中我了解到,类已经或至少可以用于构成一层抽象,以便于代码的轻松回收,然后可以将其用于创建对象或用于继承。

例如,我可以拥有一个动物类别,然后从中继承猫和狗,使它们都继承许多相同的特征,然后从那些子类别中,我可以创建可以指定动物品种甚至名称的对象它的。
或者,我可以使用类来指定同一代码的多个实例,这些实例处理或包含略有不同的事物。例如搜索树中的节点或多个不同的数据库连接,而没有。

我最近进入函数式编程领域,因此我开始怀疑:
纯函数式语言如何处理此类问题?也就是说,语言没有任何类和对象的概念。


1
您为什么认为函数并不意味着类?一些第一类来自LISP- CLOS。 查看clojure 命名空间类型模块以及haskell

我指的是不具有类的功能语言,我非常了解少数几种语言
电动咖啡

1
以Caml为例,其姊妹语言OCaml添加了对象,但是Caml本身没有对象。
电动咖啡

12
术语“纯功能”是指保持引用透明性且与该语言是否具有任何面向对象的功能无关的功能语言。
sepp2k

2
可惜的是,OO中的代码重用远比FP中的困难。多年来,OO声称拥有代码重用的所有功能,但我看到它会持续最少的时间。(随意地说我一定做错了,我对我不得不多年设计和维护OO系统而编写的OO代码感到满意,我知道我自己的结果的质量)
Jimmy Hoffa

Answers:


19

许多功能语言都有模块系统。(顺便说一下,许多面向对象的语言也是如此。)但是即使没有一种语言,也可以将函数用作模块。

JavaScript是一个很好的例子。在JavaScript中,函数既用于实现模块,也用于实现面向对象的封装。在Scheme(这是JavaScript的主要灵感)中,只有函数。函数几乎用于实现所有功能:对象,模块(在Racket中称为单元),甚至数据结构。

OTOH,Haskell和ML系列具有显式的模块系统。

面向对象是关于数据抽象的。而已。模块化,继承,多态甚至可变状态都是正交的问题。


8
您能解释这些事情如何与oop有关的更多细节吗?而不是简单地声明概念存在……
电动咖啡

旁注-模块和单元是Racket中的两种不同的构造-模块可与名称空间媲美,而单元则介于名称空间和OO接口之间。文档更加详细地介绍了不同之处
杰克

@Jack:我不知道Racket也有一个叫做的概念module。我认为不幸的是,Racket的概念module不是模块,而是模块而不是module。无论如何,您写道:“单元在名称空间和OO接口之间有点中间”。好吧,这不是关于模块是什么的定义吗?
约尔格W¯¯米塔格

模块和单元都是绑定到值的名称组。模块可以依赖于其他特定的绑定集,而单元可以依赖于使用该单元的任何其他代码必须提供的一些常规绑定集。单位是通过绑定参数化的,模块则不是。依赖于绑定的模块和依赖于绑定map的单元map是不同的,因为该模块必须引用某些特定的map绑定,例如来自的绑定racket/base,而该单元的不同用户可以为该单元提供不同的定义map
2014年

4

您似乎在问两个问题:“如何在功能语言中实现模块化?” 其他答案和“如何用功能语言创建抽象?”中已解决了这些问题。我会回答。

在OO语言中,您倾向于专注于名词,“动物”,“邮件服务器”,“他的花园叉”等。相反,功能语言则强调动词“走路”,“获取邮件”。 ,“生产”等。

因此,功能语言中的抽象倾向于动词或操作而不是事物,这也就不足为奇了。当我试图解释这一点时,我总是能举一个例子。在函数式语言中,编写解析器的一种好方法是指定一种语法,然后对其进行解释。解释器在解析过程中创建一个抽象。

另一个具体的例子是我不久前从事的一个项目。我在Haskell中编写数据库。我只有一种“嵌入式语言”用于指定最低级别的操作;例如,它允许我从存储介质中读写内容。我还有另一种单独的“嵌入式语言”,用于指定最高级别的操作。然后,我有了本质上是解释器的设备,用于将操作从较高级别转换为较低级别。

这是一种非常通用的抽象形式,但它并不是功能语言中唯一可用的一种。


4

尽管“函数式编程”并未传达出对模块化问题的深远影响,但是特定语言以不同的方式解决了大型编程问题。代码重用和抽象的交互作用在于,暴露的次数越少,重用代码的难度就越大。抛开抽象,我将解决两个可重用性问题。

静态类型的OOP语言传统上使用名义子类型,这意味着为类/模块/接口A设计的代码仅在B明确提及A时才能处理类/模块/接口B。函数式编程族中的语言主要使用结构子类型。这样,只要B具有A的所有方法和/或字段,为A设计的代码就可以处理B。在需要更通用的类/接口A之前,B可能是由另一个团队创建的。例如,在OCaml中,结构子类型化适用于模块系统,类似OOP的对象系统及其非常独特的多态变体类型。

OOP和FP wrt之间最显着的区别。模块化是指OOP中的默认“单位”作为对象捆绑在一起,对相同值的情况进行各种运算,而FP中的默认“单位”捆绑在一起,作为函数针对各种情况的值进行相同操作。在FP中,将操作捆绑在一起(例如作为模块)仍然非常容易。(顺便说一句,Haskell和F#都没有成熟的ML系列模块系统。)表达式问题任务是增量地添加对所有值都起作用的新操作(例如,将新方法附加到现有对象上)以及所有操作应支持的值的新情况(例如,添加具有相同接口的新类)。如下面的第一篇Ralf Laemmel讲座所述(在C#中有大量示例),在OOP语言中添加新操作是有问题的。

Scala中OOP和FP的结合可能使其成为功能最强大的语言之一。模块化。但是OCaml仍然是我最喜欢的语言,而且从我个人的主观观点来看,它并不比Scala差。下面的两次Ralf Laemmel演讲讨论了Haskell中表达问题的解决方案。我认为这种解决方案虽然可以很好地工作,但是却很难使用具有参数多态性的结果数据。在下面链接的Jaques Garrigue文章中介绍了使用OCaml中的多态变体解决表达问题的方法,没有这个缺点。我还链接到教科书的章节,这些章节比较了OCaml中非OOP和OOP模块化的用法。

以下是Haskell和OCaml特定的链接,它们在表达问题上进行了扩展:


2
您介意对这些资源的用途进行更多解释吗?为什么建议您在回答所提问题时推荐这些资源?在Stack Exchange上不太欢迎“仅链接的答案”
gna13 2013年

2
我只是提供了一个实际的答案,而不仅仅是提供链接作为编辑。
lukstafi 2013年

0

实际上,OO代码的可重用性要少得多,这是设计使然。OOP背后的想法是将对特定数据段的操作限制为类中或继承层次结构中适当位置的某些特权代码。这限制了可变性的不利影响。如果数据结构发生变化,则代码中只有太多地方可以负责。

有了不变性,您不必在乎谁可以对任何给定的数据结构进行操作,因为没有人可以更改您的数据副本。这使得创建新功能来处理现有数据结构变得更加容易。您只需创建功能并将它们分组为从域角度看似乎合适的模块。您不必担心将它们放入继承层次结构的位置。

另一类代码重用是创建新的数据结构以使用现有功能。这是使用泛型和类型类之类的功能以功能语言处理的。例如,Haskell的Ord类型类允许您对sort具有Ord实例的任何类型使用该函数。如果实例不存在,则很容易创建它们。

以您的Animal示例为例,并考虑实现供稿功能。直接的OOP实现是维护Animal对象的集合,并遍历所有对象,并feed在每个对象上调用方法。

但是,当您深入细节时,事情会变得棘手。一个Animal对象自然知道什么样的食物它吃,它以需要多大的感觉十足。它自然知道食物存放在哪里以及有多少可用,因此FoodStore对象只是成为了Every的依赖项Animal,要么作为Animal对象的字段,要么作为feed方法的参数传入。或者,为使Animal类保持更高的凝聚力,您可以移至feed(animal)FoodStore对象,也可以创建一个称为an AnimalFeeder或此类的类的可憎对象。

在FP中,不倾向于Animal始终将字段组合在一起,这对于可重用性具有一些有趣的含义。假设你有一个清单Animal记录,与领域,如namespecieslocationfood typefood amount,等你也有一个列表FoodStore记录中包含的字段locationfood type以及food amount

喂养的第一步可能是将每个记录列表映射到(food amount, food type)成对列表,其中动物数量为负数。然后,您可以创建函数来使用这些对进行各种操作,例如将每种食物的数量相加。这些功能并不是Animal一个FoodStore模块或模块的完美结合,但是两者都可以高度复用。

最后,您会得到一堆函数,这些函数可做有用的事情,[(Num A, Eq B)]这些函数是可重用的和模块化的,但是您很难确定将它们放置在何处或将它们称为什么。结果是FP模块更难以分类,但分类的重要性要低得多。


-1

流行的解决方案之一是将代码分成模块,这是在JavaScript中的实现方式:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

完整的文章解释JavaScript的这种模式,除了有其他方式来定义模块,如数字RequireJSCommonJS的,谷歌关闭。另一个示例是Erlang,其中您既具有模块又具有强制执行A​​PI和模式的行为,它们的作用与OOP中的Interfaces相似。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.