纯函数式编程中的“价值”是啥?

Posted

技术标签:

【中文标题】纯函数式编程中的“价值”是啥?【英文标题】:What is "value" in pure functional programming?纯函数式编程中的“价值”是什么? 【发布时间】:2018-10-30 02:22:04 【问题描述】:

什么构成纯函数式编程中的值?

看到一句话,我问自己这些问题:

Task(或IO)有一个构造函数,可以将副作用捕获为

函数是值吗? 如果是这样,将两个函数等同起来是什么意思:assert(f == g)。对于两个等效但分别定义的函数 => f != g,为什么不将它们用作 1 == 1? 具有方法的对象是值吗? (例如IO println("") ) 具有 setter 方法和可变状态的对象是值吗? 作为状态机工作的具有可变状态的对象是一个值吗?

我们如何测试某个东西是否是一个值?不变性是充分条件吗?

更新: 我正在使用 Scala。

【问题讨论】:

无法测试函数是否相等,但这并不意味着它们不能是值。值是存在于类型中的东西。 您可以为函数定义相等关系。例如。定义相同源代码的两个函数是相等的。定义具有相同 AST 的两个函数是相等的。定义具有相同 AST 模重命名的两个函数是相等的。您可以做的是决定两个函数是否在所有情况下计算相同的结果。 下面的答案很好,但要注意:有些文献使用“值”来对比类型级术语和计算级术语(如下所述),但有些文献使用“值”来对比在可能未评估/错误的术语和完全评估的非错误术语之间。在这两种情况下,第二个选择都是所谓的“价值”。 值基本上是 beta-normal 形式(即不能进一步减少)。 我知道这过于简单化了,但通常我认为“值”是可以分配/绑定到变量的任何东西。 【参考方案1】:

价值观是

函数可以作为输入并作为输出返回,也就是说,可以计算,并且 是一种类型的成员,即某个集合的元素,并且 可以绑定变量,也就是可以命名。

第一点确实是关键测试某物是否有价值。也许value这个词,由于条件作用,可能会立即让我们想到数字,但这个概念非常笼统。本质上,我们可以给予和退出函数的任何东西都可以被认为是一个值。数字、字符串、布尔值、类的实例、函数本身、谓词,甚至类型本身,都可以是函数的输入和输出,因此是值。

IO monad 是一个很好的例子,说明了这个概念的普遍性。当我们说IO monad 将副作用建模为值时,我们的意思是一个函数可以将副作用(比如println)作为输入并作为输出返回。 IO(println(...))println 的动作的效果 概念与动作的实际执行分开,并允许将这些效果视为可以使用相同的方法计算的第一类值语言设施与任何其他值(例如数字)一样。

【讨论】:

等同于“类型”和“集合”对于那些需要最基本介绍的人来说可能是一个非常粗略的初步近似,但是因为我们现在有一堆具有强大类型系统的函数式编程语言 多态函数,你必须记住Polymorphism is not set-theoretic。【参考方案2】:

值是

    不可变/永恒 匿名 语义透明

42 的值是多少? 42、new Date()的“价值”是什么? Date object at 0x3fa89c3。 42的身份是什么? 42、new Date()的身份是什么?正如我们在前面的例子中看到的,它是生活在这个地方的东西。它在不同的上下文中可能有许多不同的“价值”,但它只有一个身份。 OTOH,42 本身就足够了。询问 42 在系统中的位置在语义上毫无意义。 42的语义是什么?大小为42。new Foo()的语义是什么?谁知道呢。

我会添加第四个标准(在某些情况下可以看到这一点,但在其他情况下不会),即:值与语言无关(我不确定前 3 个是否足以保证这一点也不是说这样的规则完全符合大多数人对价值意味着什么的直觉)。

【讨论】:

【参考方案3】:

我将尝试通过将其与非值的事物进行对比来解释什么是

粗略地说,评估过程产生的结构,对应于无法进一步简化的术语


条款

首先,术语是什么?术语是可以评估的句法结构。诚然,这有点循环,所以让我们看几个例子:

    常量字面量是术语:

    42
    

    应用于其他术语的功能是术语:

    atan2(123, 456 + 789)
    

    函数字面量是术语

    (x: Int) => x * x
    

    构造函数调用是术语:

    Option(42)
    

对比:

    类声明/定义不是术语:

    case class Foo(bar: Int)
    

    也就是说,你不能写

    val x = (case class Foo(bar: Int))
    

    这将是非法的。

    同样,特征和类型定义不是术语:

    type Bar = Int
    sealed trait Baz
    

    与函数字面量不同,方法定义不是术语:

    def foo(x: Int) = x * x
    

    例如:

    val x = (a: Int) => a * 2 // function literal, ok
    val y = (def foo(a: Int): Int = a * 2) // no, not a term
    

    包声明和导入语句不是术语:

    import foo.bar.baz._ // ok
    List(package foo, import bar) // no
    

范式、值

现在,当希望更清楚术语是什么时,“不能进一步简化*”是什么意思?在理想化的函数式编程语言中,您可以定义什么是正常形式,或者更确切地说是弱头范式。本质上,如果不能对术语应用简化规则以使其更简单,则术语处于(wh-)范式。再举几个例子:

    这是一个术语,但它不是正常形式,因为它可以简化为42

    40 + 2
    

    这不是弱头范式:

    ((x: Int) => x * 2)(3)
    

    因为我们可以进一步评估它为6

    这个 lambda 处于弱头正常形式(它被卡住了,因为在提供 x 之前计算无法继续):

    (x: Int) => x * 42
    

    这不是正常的形式,因为它可以进一步简化:

    42 :: List(10 + 20, 20 + 30)
    

    这是正常形式,不可能进一步简化:

    List(42, 30, 50)
    

因此,

42, (x: Int) => x * 42, List(42, 30, 50)

是值,而

40 + 2, ((x: Int) => x * 2)(3), 42 :: List(10 + 20, 20 + 30)

不是值,而是可以进一步简化的非规范化术语。


示例和非示例

我将一一浏览您的子问题列表:

函数是值吗

是的,像 (x: T1, ..., xn: Tn) => body 这样的东西在 WHNF 中被认为是卡住的术语,在函数式语言中它们实际上可以表示,所以它们是值。

如果是这样,将两个函数等同起来是什么意思:assert(f == g) 表示两个等效但分别定义的函数 => f != g,为什么它们不作为 1 == 1 工作?

函数可扩展性与某事物是否为值的问题有些无关。在上面的“示例定义”中,我只谈到了术语的形状,而不是关于在这些术语上定义的一些可计算关系的存在/不存在。可悲的事实是,您甚至无法真正确定 lambda 表达式是否真的代表一个函数(即它是否对所有输入都终止),而且众所周知,没有一种算法可以确定两个函数是否产生所有输入的输出相同(即外延相等)。

具有方法的对象是值吗? (例如IO println("")

不太清楚你在这里问什么。对象没有方法。类有方法。如果您的意思是方法调用,那么,不,它们是可以进一步简化的术语(通过实际运行方法),因此它们不是值。

具有 setter 方法和可变状态的对象是值吗? 作为状态机工作的具有可变状态的对象是值吗?

纯函数式编程中没有这样的东西。

【讨论】:

我认为您没有正确使用 WHNF 概念。用严格的语言来说,这是一个无关紧要的概念,因为无论如何,如果评估的话,一切都会被带到 NF 中。在 Haskell 中,42 : [10 + 20, 20 + 30] 在 WHNF 中。对于(x: Int) => x * 42,区别是无关紧要的,因为这在 WHNF 和 NF 中都是如此。然而,我认为范式应该完全排除在讨论之外——一个值在被评估之前已经是一个值,但是评估不会改变它。即,值是术语的等价类。如果你不同意,你应该提供一些参考。【参考方案4】:

与命令式语言形成鲜明对比。在诸如 Python 之类的 inperitive 语言中,函数的输出是定向的。它可以分配给变量、显式返回、打印或写入文件。

当我在 Haskell 中编写函数时,我从不考虑输出。我从不使用“return” 一切都有“a”值。这称为“符号”编程。 “一切”的意思是“符号”。就像人类语言一样,名词和动词代表着某种东西。那就是他们的价值。 “皮特”的“价值”是皮特。 “皮特”这个名字不是皮特,而是皮特这个人的代表。函数式编程也是如此。最好的类比是数学或逻辑当你做几页计算时,你是否指导每个函数的输出?您甚至可以在函数或表达式中“分配”变量以替换为它们的“值”。

【讨论】:

【参考方案5】:

什么构成纯函数式编程中的值?

背景

函数式编程中没有突变。因此,诸如

之类的代码
case class C(x: Int)

val a = C(42)
val b = C(42)

会变成等价于

case class C(x: Int)

val a = C(42)
val b = a

因为在函数式编程中,如果a.x == b.x,那么我们将有a == b。也就是说,a == b 将实现比较内部的值。

然而,Scala 不是纯粹的,因为它允许变异,就像 Java 一样。在这种情况下,当我们声明 case class C(var x: Int) 时,我们没有上述两个 sn-ps 之间的等价性。实际上,执行a.x += 1 afterwords 不会影响第一个sn-p 中的b.x,但会影响第二个sn-p,其中ab 指向同一个对象。在这种情况下,比较 a == b 比较对象 references 而非其内部整数值是很有用的。

使用case class C(x: Int) 时,Scala 比较a == b 的行为更接近纯函数式编程,比较整数值。对于常规(非case)类,Scala 会比较对象引用,从而破坏两个 sn-ps 之间的等价性。但是,Scala 也不是纯粹的。相比之下,在 Haskell 中

data C = C Int deriving (Eq)
a = C 42
b = C 42

确实等价于

data C = C Int deriving (Eq)
a = C 42
b = a

因为 Haskell 中没有“引用”或“对象标识”。请注意,Haskell 实现可能会在第一个 sn-p 中分配两个“对象”,而在第二个中仅分配一个对象,但由于在 Haskell 内部无法区分它们,因此程序输出将是相同的。

回答

函数是值吗? (那么当两个函数相等时意味着什么:assert(f==g)。对于两个等价但分别定义的函数 => f!=g,为什么它们不像 1==1 那样工作)

是的,函数是纯函数式编程中的值。

上面,当您提到“等效但单独定义的函数”时,您假设我们可以比较这两个函数的“引用”或“对象标识”。在纯函数式编程中我们不能。

纯函数式编程应该比较使f == g 等效于f x == g x 的所有可能参数x 的函数。当x 只有几个值时,这是可行的,例如如果f,g :: Bool -> Int 我们只需要检查x=True, x=False。对于具有无限域的函数,这要困难得多。例如,如果f,g :: String -> Int 我们不能检查无限多的字符串。

理论计算机科学(可计算性理论)也证明了没有算法可以比较两个函数String -> Int,甚至不是低效的算法,即使我们可以访问这两个函数的源代码。由于这个数学原因,我们必须接受函数是无法比较的值。在 Haskell 中,我们通过 Eq 类型类来表达这一点,指出几乎所有标准类型都是可比较的,函数除外。

具有方法的对象是值吗? (例如,IOprintln(""))

是的。粗略地说,“一切都是价值”,包括 IO 动作。

具有 setter 方法和可变状态的对象是值吗? 具有可变状态并作为状态机工作的对象是值吗?

纯函数式编程中没有可变状态。

setter 最多可以生成一个带有修改字段的“新”对象。

是的,对象将是一个值。

我们如何测试它是否是一个值,不可变是否可以成为一个值的充分条件?

在纯函数式编程中,我们只能拥有不可变的数据。

在不纯函数式编程中,我认为我们可以将大多数不可变对象称为“值”,当我们不比较对象引用时。如果“不可变”对象包含对可变对象的引用,例如

case class D(var x: Int)
case class C(c: C)
val a = C(D(42))

那么事情就更棘手了。我想我们仍然可以将a 称为“不可变”,因为我们不能更改a.c,但我们应该小心,因为a.c.x 可以被变异。 根据意图,我认为有些人不会调用a 不可变。我不会认为a 是一个值。

为了让事情变得更加混乱,在不纯的编程中,有些对象使用突变以有效的方式呈现“纯”接口。例如,可以编写一个纯函数,在返回之前将其结果存储在缓存中。当在同一个参数上再次调用时,它将返回先前计算的结果 (这通常称为memoization)。在这里,发生了突变,但从外部无法观察到,我们最多可以观察到更快的实现。在这种情况下,我们可以简单地假设该函数是纯函数(即使它执行突变)并将其视为“值”。

【讨论】:

"即使使用 case class C(x: Int),Scala 仍然在使用 a == b 时比较对象引用" Scala 比较的是 case classes 的内容,而不是引用。 @Kolmar 谢谢,我忘了。我试图进行相应的修改。 关于可变对象,它需要有一个标识。这个身份(例如可变内存的位置)可以用一个值(例如指针)来表示。

以上是关于纯函数式编程中的“价值”是啥?的主要内容,如果未能解决你的问题,请参考以下文章

函数式编程中的纯函数

纯函数式编程语言中的双向链表

函数式编程

函数式编程中的 “函数们”

前端学习之函数式编程—纯函数

python中的函数式编程与装饰器