函数编程中的不变性真的存在吗?


9

尽管我在日常生活中是一名程序员,并且使用所有流行的语言(Python,Java,C等),但对于什么是函数式编程,我仍然不清楚。根据我的阅读,功能语言的一个特性是数据结构是不可变的。对我来说,这本身就引发了很多问题。但是首先,我会写一些我对不变性的理解,如果我错了,请随时纠正我。

我对不变性的理解:

  • 程序启动时具有固定的数据结构和固定的数据
  • 无法将新数据添加到这些结构
  • 代码中没有变量
  • 您只能从已经存在的数据或当前计算出的数据中“复制”
  • 由于以上原因,不变性给程序增加了巨大的空间复杂性

我的问题:

  1. 如果数据结构应该保持不变(不可变),那么有人怎么在列表中添加新项目呢?
  2. 拥有无法获取新数据的程序有什么意义?假设您有一个连接到计算机的传感器,该传感器想将数据提供给程序。那是否意味着我们无法在任何地方存储传入的数据?
  3. 在这种情况下,函数式编程对机器学习有什么好处?由于机器学习是基于更新程序对事物的“感知”的假设而建立的,因此可以存储新数据。

2
当您说功能代码中没有变量时,我不同意您的看法。从数学意义上讲,变量是“可以假定一组值中的任何一个的量”。当然,它们不是可变的,但在数学上也不是。
爱德华

1
我认为您的困惑是因为您正在抽象地考虑功能语言。只需在Haskell中使用任何程序(例如,从控制台读取数字列表,对其进行快速排序并将其输出的程序),并弄清楚它的工作方式以及它如何证明您的怀疑。如果不查看实际程序的示例而不是进行哲学化处理,就无法真正弄清一切。您可以在任何Haskell教程中找到大量程序。
jkff

@jkff你想说什么?哈斯克尔具有非功能性特征。问题不在于Haskell,而是函数式编程。还是您要断言所有功能都起作用?怎么样?正如您所说,哲学化应该出什么问题。抽象以什么方式引起混淆?OP问题是一个非常明智的问题。
babou 2015年

@babou我想说的是,了解纯函数式编程语言如何有效实现算法和数据结构的最佳方法是查看以函数式编程语言有效实现的算法和数据结构的示例。在我看来,OP试图理解其在概念上的可能性-我认为理解这一点的最快方法是看示例,而不是阅读概念性的解释,无论它多么详细。
jkff 2015年

看功能编程的一种方法是说它是没有副作用的编程。您可以使用自己的“时髦”语言来做到这一点。只是避免所有的重新分配:例如,在Java中,所有变量都是最终变量,而所有方法均为只读。
reinierpost

Answers:


10

程序启动时具有固定的数据结构和固定的数据

这有点误解。它具有固定的形式和一组固定的重写规则,但是这些重写规则可能会爆炸成更大的东西。例如,Haskell中的表达式[1..100000000]用很少的代码表示,但是其正常形式却很大。

无法将新数据添加到这些结构

是的,没有。Haskell或ML之类的语言的纯功能子集无法从外界获取数据,但是任何用于实际编程的语言都有一种机制,可以将外界的数据插入到纯功能子集中。在Haskell中,这非常小心地完成,但是在ML中,您可以随时执行此操作。

代码中没有变量

这几乎是正确的,但是不要将其与无法命名的想法混淆。您一直在命名有用的表达式,并不断重复使用它们。ML和Haskell,我尝试过的每一个Lisp,以及Scala等混合动力,都具有创建变量的方法。它们只是不常用。同样,此类语言的纯功能子集也没有它们。

您只能从已经存在的数据或当前计算出的数据中“复制”

您可以通过简化为正常形式来执行计算。最好的办法可能是用一种功能语言编写程序,看看它们实际上如何执行计算。

例如,“ sum [1..1000]”不是我想要执行的计算,但是它由Haskell轻松完成。我们给了它一个对我们有意义的小表达,Haskell给了我们相应的数字。因此,它肯定会执行计算。

如果数据结构应该保持不变(不可变),那么有人怎么在列表中添加新项目呢?

您无需在列表中添加新项目,而是在旧列表的基础上创建一个新列表。由于旧版本无法进行突变,因此在新列表中或您想要的其他任何位置使用它都是绝对安全的。在此架构中,可以安全地共享更多数据。

拥有无法获取新数据的程序有什么意义?假设您有一个连接到计算机的传感器,该传感器想将数据提供给程序。那是否意味着我们无法在任何地方存储传入的数据?

就用户输入而言,任何实用的编程语言都可以获取用户输入。有时候是这样的。但是,您可以使用大多数语言来编写这些语言的功能齐全的子集,并以此方式获得好处。

在这种情况下,函数式编程对机器学习有什么好处?由于机器学习是基于更新程序对事物的“感知”的假设而建立的,因此可以存储新数据。

主动学习就是这种情况,但是我所使用的大多数机器学习(我作为机器学习小组中的代码猴子而工作了几年)都具有一次性学习过程,其中加载了所有训练数据立刻进入。但是,对于主动学习,您不能100%纯粹地从功能上做事情。您将不得不从外界读取一些数据。


我觉得您很方便地忽略了@Pithikos帖子中最重要的一点,那就是空间问题–功能性程序比命令性程序使用更多的空间(您不能编写就地算法等)
user541686

2
这根本不是真的。缺乏突变主要是通过共享来弥补的,并且最重要的是,您所指的大小差异在现代编译器中绝对很小。haskell列表中的大多数代码都有效地存在或根本不使用任何内存。
2015年

1
我认为您在某种程度上歪曲了ML。是的,I / O可以在任何地方发生,但是将新信息引入现有结构的方式受到严格控制。
dfeuer 2015年

@Pithikos,到处都是变量;正如Édouard所指出的,它们与您习惯的有所不同。事情不断被分配和垃圾收集。一旦真正进入函数式编程,您就会对它的实际运行情况有更好的了解。
dfeuer 2015年

1
确实存在不存在具有与最著名的命令式执行相同的时间复杂度的纯功能性实现的算法-例如,Union-Find数据结构(以及um,arrays :))我想对于空间,也有类似情况复杂。但是这些都是例外-大多数常见的算法/数据结构的实现都具有相同的时间和空间复杂度。这是编程风格和编译器质量的一个主观问题。
jkff 2015年

4

不变性或可变性不是在函数编程中有意义的概念。

计算上下文

这是一个很好的问题,是对另一个最近的问题的有趣的跟进(不是重复):分配,评估和名称绑定之间有什么区别?

与其逐个答复您的陈述,不如说是在这里给您一个结构化的概述。

您可以考虑以下几个问题,包括:

  • 什么是计算模型,什么概念对给定模型有意义

  • 您使用的单词的含义是什么,它如何取决于上下文

函数式编程风格似乎很愚蠢,因为您用命令式的眼神看到了它。但这是一个不同的范例,您的命令性概念和感知是陌生的,与时俱进。编译器没有这种偏见。

但是最后的结论是,有可能以纯粹的功能性方式编写程序,包括用于机器学习的功能,认为功能性编程不具有存储数据的概念。在这一点上,我似乎不同意其他答案。

希望尽管这个答案很长,但仍会有一些人感兴趣。

计算范式

问题是函数编程(又称应用程序编程),这是一种特定的计算模型,其理论上最简单的代表是lambda演算。

如果您仍处于理论水平,则有许多计算模型:图灵机​​(TM),RAM机,拉姆达演算,组合逻辑,递归函数理论,半Thue系统等。计算功能越强大事实证明,这些模型可以解决的问题是等效的,这就是Church-Turing论文的要旨 。

一个重要的概念是相互简化模型,这是建立等价关系的基础,该等价关系导致了Church-Turing论文。从程序员的角度来看,将一种模型简化为另一种模型通常称为编译器。如果您将逻辑编程作为计算模型,则它与您在商店中购买的PC提供的模型完全不同,并且编译器会将以逻辑编程语言编写的程序转换为PC代表的计算模型(相当多)。 RAM计算机)。

但是,这并不意味着这两个模型以相同的方式进行操作,也不意味着可以将对一个人有意义的概念转移给另一个人。通常,TM中的计算步骤与Lambda演算中的()还原步骤几乎没有关系,尽管它们可以相互翻译。Lambda表达的最佳评估概念与TM模型中的复杂性问题相去甚远。β

实际上,我们使用的编程语言倾向于混合来自不同理论渊源的概念,并试图做到这一点,以使程序的选定部分可以在适当的情况下受益于某些模型的属性。同样,人员构建系统可以为不同的组件选择不同的语言,以使该语言最适合手头的任务。

因此,您很少会看到以编程语言处于纯状态的编程范例。编程语言仍然根据主要的范式进行分类,但是当涉及其他范式的概念时,语言的属性可能会受到影响,这常常使区分和概念问题变得模糊。

通常,诸如Haskell和ML或CAML之类的语言被认为是功能性的,但是它们可以允许命令式的行为……否则为什么有人会谈到“ 纯功能子集 ”呢?

然后可以声称,可以用我的函数式编程语言来做到这一点,也可以用我的函数式编程语言来做到这一点,但是当函数式编程依赖于可以被认为是额外功能的东西时,它并没有真正回答关于函数式编程的问题。

答案应该与特定范式更精确地相关,而没有额外内容。

什么是变量?

另一个问题是术语的使用。在数学中,变量是一个实体,在某些领域中代表未确定的值。它用于各种目的。在方程式中使用时,它可以代表任何值,以便验证方程式。在逻辑编程中以“ 逻辑变量 ” 的名称使用了这种构想,这可能是因为在开发逻辑编程时,名称变量已经具有另一种含义。

在传统的命令式编程中,变量被理解为某种容器(或存储位置),可以存储值的表示形式,并可能将其当前值替换为另一个值。

在函数式编程中,变量在数学上的作用与作为占位符的目的相同,尚有待提供。在传统的命令式编程中,此角色实际上是由常量扮演的(不要与文字混淆,这些文字是用特定于该值域的符号表示的值,例如123,true,[“ abdcz”,3.14])。

任何种类的变量以及常量都可以由标识符表示。

命令式变量可以更改其值,这是可变性的基础。功能变量不能。

编程语言通常允许从语言中的较小实体构建较大的实体。

命令式语言允许此类构造包含变量,这就是为您提供可变数据的原因。

如何阅读程序

从根本上说,程序是对算法的抽象描述,它是某种语言,无论是实用设计还是范式纯净的语言。

原则上,您可以接受每一个抽象的说法。然后,编译器会将其转换为某种适合计算机执行的格式,但这并不是您首先遇到的问题。

当然,现实会更加苛刻,通常最好对发生的事情有所了解,以避免编译器将不知道如何处理的结构有效执行。但这已经是优化……编译器可能非常适合,通常比程序员更好。

功能编程和可变性

可变性基于命令变量的存在,该命令变量可以包含值,可以通过赋值进行更改。由于这些在函数式编程中不存在,因此一切都可以视为不可变的。

功能编程专门处理值。

关于不变性的前四个陈述大部分是正确的,但是用命令式视图描述了一些非必要性。这有点像在每个人都是盲人的世界中用颜色描述。您正在使用与函数式编程无关的概念。

您只有纯值,而整数数组是纯值。要获得仅在一个元素上不同的另一个数组,您必须使用不同的数组值。更改元素只是在此上下文中不存在的概念。您可能有一个函数,该函数具有一个数组和一些索引作为参数,并且返回的结果是几乎相同的数组,仅在索引指示的位置不同。但是它仍然是一个独立的数组值。这些值的表示方式不是您的问题。也许他们在计算机的命令翻译中“共享”了很多东西……但这是编译器的工作……而您甚至不想知道它正在编译哪种机器体系结构。

您不复制值(没有意义,这是一个外来的概念)。您只需要使用在程序中定义的域中存在的值即可。您要么描述它们(作为文字),要么它们是将函数应用于其他值的结果。您可以给它们命名(从而定义一个常数),以确保在程序的不同位置使用相同的值。注意,函数应用程序不应被视为计算,而应视为对给定参数的应用程序的结果。写作5+2或写作7数额相同。与上一段是一致的。

没有命令式变量。无法分配。您只能将名称绑定到值(以形成常量),这不同于命令式语言,您可以将名称绑定到可分配的变量。

这是否会带来复杂性,目前尚不清楚。一方面,您提到的复杂性涉及命令式范例。除非您选择将功能性程序作为命令性程序阅读,否则这并不是功能性编程中的定义,这不是设计者的意图。实际上,功能视图旨在让您不必担心此类问题,而将精力集中在正在计算的内容上。它有点像规范与实现。

编译器必须注意实现,并且要足够聪明,以使其最适合要执行的硬件,无论它是什么。

我并不是说程序员从不为此担心。我并不是说编程语言和编译器技术已经如我们希望的那样成熟。

回答问题

  1. 您无需修改​​现有值(外来概念),但可以根据需要计算出不同的新值,这可能是因为它包含一个额外的元素(它是一个列表)。

  2. 该程序可以获取新数据。重点是您用语言表达的方式。例如,您可以考虑该程序使用一个特定的值(可能大小不受限制)工作,该值称为输入流。这个值应该放在这里(无论是否已经完全知道不是您的问题)。然后,您将具有一个函数,该函数返回由流的第一个元素和流的其余部分组成的一对。

    您可以使用它以纯粹适用的方式构建通讯组件网络(协程)

  3. 当您必须累积数据和修改值时,机器学习只是另一个问题。在函数式编程中,您无需这样做:您只需根据训练数据计算出不同的新值即可。生成的机器也将正常工作。您担心的是计算时间和空间效率。但是,这又是一个不同的问题,理想情况下应由编译器处理。

结束语

从注释或其他答案中可以很清楚地看出,实用的函数式编程语言并非纯粹是功能性的。这反映了我们的技术仍有待改进的事实,尤其是在编译方面。

可以采用纯粹的应用风格进行写作吗?答案已经知道了大约40年,是的。指代语义学的真正目的恰恰是在1970年代出现的,目的是将语言翻译(编译)成纯函数式风格,被认为可以更好地理解数学,因此被认为是定义程序语义的更好基础。

有趣的是,可以通过引入适当的值域(例如数据存储区)将命令式编程结构(包括变量)转换为功能样式。尽管具有功能风格,但它仍然惊人地类似于以命令式风格编写的实际编译器代码。


0

误解是功能程序无法存储数据,我认为Jakes的回答并不能很好地解释这一点。

像任何程序一样,函数程序实际上是将整数映射到整数的函数。在可变数据结构上运行的任何命令式程序都具有功能上的对应物。这只是达到相同目的的另一种方法。

从某个来源存储实验数据的功能方式是调用以数据结构作为参数的存储函数,并输出现有数据结构和新数据的串联,因此数据存储时没有可变数据结构的概念。

根据我自己的经验,我认为不可变数据结构的概念使传统开发人员认为在功能设置中某些事情是不切实际甚至无法完成的。不是这种情况。


“函数程序就像任何程序一样,实际上是将整数映射为整数的函数。” 例如,Minecraft到底是如何将整数映射为整数的函数?
David Richerby

容易。每个字节都可以解释为二进制整数。计算机中的状态是字节的集合。一个程序,甚至是Minecraft,都可以操纵计算机状态,将其从一种状态映射到另一种状态。
2015年

用户输入似乎不适合这个世界。
David Richerby,2015年

用户输入是计算机状态的一部分。它不仅存在于您的屏幕上。
2015年
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.