在纯函数式编程中,“value”是什么?

10
在纯函数式编程中,什么构成了值?
看到一个句子后,我思考这些问题:
Task(或IO)有一个构造函数,将副作用捕获为值。
以下是我的问题:
1. 函数是值吗?
如果是,那么当等同于两个函数时会发生什么:`assert(f == g)`。对于两个定义不同但等效的函数=> `f!= g`,为什么它们不能像`1 == 1`一样工作?
2. 带有方法的对象是值吗?(例如:`IO { println("") }`)
3. 带有setter方法和可变状态的对象是值吗?
4. 作为状态机运行的具有可变状态的对象是值吗?
我们如何测试某物是否为值? 不可变性是否足够条件?
更新:我正在使用Scala。

13
函数无法进行相等性测试,但这并不意味着它们不能成为值。值是类型中存在的东西。 - Rein Henrichs
1
你可以为函数定义一个相等关系。例如,两个定义相同源代码的函数是相等的。两个定义具有相同AST的函数是相等的。两个定义具有相同重命名的AST的函数是相等的。但你无法确定两个函数在所有情况下是否计算出相同的结果。 - Jörg W Mittag
2
下面的答案很好,但要注意:有些文献使用“value”来区分类型级术语和计算级术语(如下所述),但有些文献使用“value”来区分可能未评估/出错的术语和完全评估的非错误术语。在这两种情况下,第二个选择被称为“value”。 - Daniel Wagner
值是基本正则形式(即不能进一步“约简”)。 - xuq01
2
我知道这么说有点简化了,但是一般来说,我认为“值”指的是可以被赋值或绑定到变量上的任何东西。 - DarthFennec
5个回答

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) 时,上述两个代码片段之间并没有等价性。实际上,在第一个片段中执行 a.x += 1 并不影响 b.x,但在第二个片段中会影响,因为 ab 指向同一个对象。在这种情况下,有一个比较 a == b ,它会比较对象引用而不是其中的整数值,是很有用的。

当使用 case class C(x: Int) 时,Scala 的比较 a == b 表现得更接近于纯函数式编程,比较整数值。对于普通(非 case)类,Scala 比较对象引用,打破了上述两个代码片段之间的等价性。但是,再次强调,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 实现可能会分配两个“对象”,而第二个只分配一个,“但由于在 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 类型类来表示这一点,即几乎所有标准类型都是可比较的,函数是例外。

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

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

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

在纯函数式编程中不存在可变状态。

最好的情况下,设置器可以生成带有修改字段的“新”对象。

是的,该对象将是一个值。

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

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

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

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

当涉及到更复杂的情况时,事情就变得更加棘手了。我想我们仍然可以称a为"不可变",因为我们无法改变a.c,但我们应该小心,因为a.c.x是可以被改变的。根据意图,我认为有些人不会将a视为不可变。我不认为a是一个值。

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


2
即使使用case class C(x: Int),Scala在使用==比较a和b时仍然会比较对象引用。Scala比较case class的内容而不是引用。 - Kolmar
@Kolmar 谢谢,我忘记了。我尝试进行修改。 - chi
关于可变对象,它需要拥有一个身份。这个身份(例如可变内存的位置)可以通过一个值(例如指针)来表示。 - Bergi

5
我将尝试通过对比非值的事物来解释“值”的概念。粗略地说,“值”是由“求值过程”产生的结构,它们对应于不能进一步简化的“术语”。

术语

首先,“术语”是可以被求值的语法结构。不可否认,这有点循环,因此让我们看几个例子:

  1. Constant literals are terms:

    42
    
  2. Functions applied to other terms are terms:

    atan2(123, 456 + 789)
    
  3. Function literals are terms

    (x: Int) => x * x
    
  4. Constructor invocations are terms:

    Option(42)
    
与此相比,可以看出:
  1. Class declarations / definitions are not terms:

    case class Foo(bar: Int)
    

    that is, you cannot write

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

    this would be illegal.

  2. Likewise, trait and type definitions are not terms:

    type Bar = Int
    sealed trait Baz
    
  3. Unlike function literals, method definitions are not terms:

    def foo(x: Int) = x * x
    

    for example:

    val x = (a: Int) => a * 2 // function literal, ok
    val y = (def foo(a: Int): Int = a * 2) // no, not a term
    
  4. Package declarations and import statements are not terms:

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

规范形式,值

现在,当一个“术语”是什么已经有些清晰了,那么“不能再进一步简化”的意思是什么呢?在理想化的函数式编程语言中,可以定义“规范形式”,或者说“弱头部规范形式”。如果一个术语无法应用任何缩减规则来使其更简单,则该术语处于(wh-)规范形式。以下是一些例子:

  1. This is a term, but it's not in normal form, because it can be reduced to 42:

    40 + 2
    
  2. This is not in weak head normal form:

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

    because we can further evaluate it to 6.

  3. This lambda is in weak head normal form (it's stuck, because the computation cannot proceed until an x is supplied):

    (x: Int) => x * 42
    
  4. This is not in normal form, because it can be simplified further:

    42 :: List(10 + 20, 20 + 30)
    
  5. This is in normal form, no further simplifications possible:

    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) ,为什么它们不像 1 == 1 那样工作呢?

函数外延性与某些东西是否是值有些无关。在上面的“举例定义”中,我只谈到了项的形状,而没有谈论某些在这些项上定义的可计算关系的存在/不存在。不幸的是,你甚至无法真正确定 lambda 表达式是否实际上表示一个函数(即它是否对所有输入终止),并且已知不能有算法来确定两个函数是否对所有输入产生相同的输出(即它们在外延上相等)。

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

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

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

在纯函数式编程中不存在这样的东西。


我认为你没有正确理解WHNF这个概念。在严格的语言中,它是一个不相关的概念,因为无论如何,如果被评估,所有东西都会被带到NF。在Haskell中,42 : [10 + 20, 20 + 30] 确实在WHNF中。对于(x: Int) => x * 42来说,区别是不相关的,因为它既在WHNF中,也在NF中。然而,我认为正常形式应该完全摆脱讨论——一个值在被评估之前已经是一个值了,但是评估不会改变它。即,一个值是等价类的术语。如果您不同意,应该提供一些参考资料。 - leftaroundabout

2
与命令式语言形成鲜明对比。在命令式语言(如Python)中,函数的输出是有方向的。它可以被分配给一个变量,显式地返回,打印或写入文件。
当我在Haskell中编写函数时,我从不考虑输出。我从不使用"return"。一切都有“一个”值。这被称为“符号化”编程。所谓的“一切”,指的是“符号”。就像人类语言一样,名词和动词代表着某些东西。那个东西就是它们的价值。 "Pete"的“价值”就是Pete本人。名称“Pete”并不是Pete,而是Pete的代表。函数式编程也是如此。最好的类比是数学或逻辑。当你做大量计算时,你会将每个函数的输出定向吗?你甚至会将变量“分配”到函数或表达式中替换为它们的“值”。

2

值具有以下特征:

  1. 不可变/永恒
  2. 匿名
  3. 语义透明

42的值是什么?是42。 new Date()的“值”是什么?是Date object at 0x3fa89c3。 42的身份是什么?是42。 new Date()的身份是什么?就像我们在上一个例子中看到的那样,它是居住在某个地方的东西。它可能在不同的上下文中有许多不同的“值”,但它只有一个身份。另一方面,42已经足够了。问42在系统中住在哪里是语义上没有意义的。42的语义意义是什么?42的大小。 new Foo()的语义意义是什么?谁知道。

我还想添加第四个标准(在某些情况下可以在现实生活中看到,但在其他情况下却不能):值是与语言无关的(我不确定前三个标准是否足以保证这一点,也不确定这样的规则是否与大多数人对值的直觉完全一致)。


0

值是函数可以接受为输入和返回为输出的东西,也就是说,它们可以被计算,并且是某种类型的成员,即一些集合的元素,也可以被绑定到一个变量,即可以被命名。

第一点是真正重要的测试,确定某个东西是否是值。也许由于习惯,单词“value” 可能立即让我们想到数字,但这个概念非常通用。基本上我们可以提供给函数并从函数中获取的任何东西都可以被认为是一个值。数字、字符串、布尔值、类的实例、函数本身、谓词甚至类型本身都可以是函数的输入和输出,因此它们都是值。

IO单子是展示这个概念的一个很好的例子。当我们说IO单子将副作用建模为值时,我们指的是一个函数可以将副作用(比如println)作为输入并返回输出。IO(println(...))效果println操作的实际执行分离开来,并允许将这些效果视为一等值进行计算,使用与任何其他值(如数字)相同的语言工具。


1
将“类型”和“集合”等同起来可能适用于那些需要最基本介绍的人,但是由于我们现在有了一堆具有强类型系统和多态函数的函数式编程语言,你必须记住多态不是集合论 - Andrey Tyukin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接