在纯函数式编程中,什么构成了一个值?
背景
在纯的函数式编程中,不存在变异。因此,像下面这样的代码:
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
,但在第二个片段中会影响,因为 a
和 b
指向同一个对象。在这种情况下,有一个比较 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
是一个值。
为了让事情更加混乱,在非纯编程中,有些对象使用变异来以高效的方式呈现“纯”接口。例如,可以编写一个纯函数,在返回之前将其结果存储在缓存中。当对相同参数再次调用时,它将返回先前计算的结果(这通常称为记忆化)。在这种情况下,发生了变异,但从外部来看是不可观察的,最多只能观察到更快的实现。在这种情况下,我们可以简单地假装函数是纯净的(即使它执行变异),并将其视为“值”。