【问题标题】:Is changing an object's variable outside of the object considered a side effect?在对象之外更改对象的变量是否被视为副作用?
【发布时间】:2020-09-03 18:22:46
【问题描述】:

我试图理解函数式编程与面向对象编程。我目前试图理解的是面向对象编程中的副作用概念,尤其是与提高程序安全性有关的概念。

我将“副作用”理解为任何更改链接到对象中的一个函数的变量,这些变量由对象使用的不同函数使用。但是,如果允许在对象之外设置对象变量,但不允许在对象内部的函数内设置对象变量呢?

在我看来,这比将其设置在另一个函数中更安全。并且该对象中使用此变量的函数会知道没有其他函数会在不通知的情况下更改它。

我错过了什么吗?你还会认为这是副作用吗?在对象初始化的时候设置一堆变量呢?

【问题讨论】:

  • anything 函数所做的对程序中的其他任何事情(或“外部世界”)有可观察到的影响 - 被视为副作用。除了副作用之外,函数唯一有用的事情就是接受参数并根据它们返回结果。换句话说,某种计算。虽然当你不习惯函数式编程时这似乎真的很有限,但像 Haskell 这样的语言证明你确实可以用纯函数编写大多数程序,并将副作用限制在绝对必要的几个地方。
  • 所以是的,改变函数中变量的值是一种副作用,至少当该变量可以从函数外部读取时 - 就像 OOP 中的实例变量一样。 Haskell 通过根本没有可变变量来解决这个问题,因此您不必担心它。
  • @RobinZigmond 绝对需要产生副作用的地方在哪里?
  • 无法避免与外界互动的地方。例如将文本/图形输出到屏幕,与数据库或文件交互,或发出网络请求等

标签: haskell functional-programming side-effects


【解决方案1】:

副作用是任何让您观察基于是否多少次的程序行为差异的东西以什么顺序评估表达式或执行操作,即破坏referential transparency。变异变量是副作用的一个例子,但在并发通道上发送消息、打印到终端、写入文件或从网络读取也是如此。

在正常的安全代码中,可观察性会产生副作用; Haskell 的运行时一直使用可变变量来进行惰性求值,但是如果没有不安全的代码,您就无法看到在语言内部。如果可能观察到与你所处的上下文相关的效果,它仍然是一个副作用。所以你描述的(限制谁可以改变对象的字段)听起来可能更安全,但它不是没有副作用的。

例如,Debug.Trace.trace :: String -> a -> a评估时会产生副作用,因为trace "x" (1 :: Int) + trace "x" (1 :: Int)let x = trace "x" (1 :: Int) in x + x 明显不同:

> trace "x" (1 :: Int) + trace "x" (1 :: Int)
x
x
2

> let x = trace "x" (1 :: Int) in x + x
x
2

modifyIORef :: IORef a -> (a -> a) -> IO ()执行时会有副作用,因为多次修改可变引用与只修改一次明显不同:

increment :: IORef Int -> IO ()
increment r = modifyIORef r (+ 1)

main :: IO ()
main = do

  r1 <- newIORef 0
  increment r1
  print =<< readIORef r1  -- 1

  r2 <- newIORef 0
  increment r2
  increment r2
  print =<< readIORef r2  -- 2

(但请注意,对于某些a,类型为IO a 的值在评估时是纯的:它不是的值输入 a “tagged” 并说明它来自 I/O;相反,它是一个 程序action返回 一个值当连接到main 并由运行时执行时,类型为a。)

请注意,并非所有有效代码都是副作用pure () :: IO ()IO 中,但显然没有副作用。同样,ST 提供 local 可变变量,保证不会逃逸或在其范围之外可见,因此您可以实现内部不纯的纯函数:

pureSum :: Int -> Int
pureSum n = sum [1 .. n]

impureSum :: Int -> IO Int
impureSum n = do
  result <- newIORef 0
  for_ [1 .. n] $ \ x -> do
    putStrLn ("Adding " ++ show x)  -- Side effect!
    modifyIORef result (+ x)
  readIORef result

internallyImpureSum :: Int -> Int
internallyImpureSum = runST $ do
  result <- newSTRef 0

  for_ [1 .. n] $ \ x -> do
    -- Can’t perform any side effects observable outside.
    modifySTRef result (+ x)

  -- Can *read* the reference, but returning
  -- the reference ‘result’ itself would be
  -- a type error.
  readSTRef result

至于“在对象初始化时设置一堆变量”,这基本上是 Haskell 中使用的模式,不仅有助于增强安全性,而且通常作为一种受数学启发的数据建模哲学。

在 OOP 语言中,对更改状态建模的惯例是创建一个单个 对象,该对象具有标识的概念,并随着时间的推移使用命令或直接突变对其进行修改。通过维护对象在每次状态更改时的所有不变量,对象应保持有效。

而在 Haskell 中,约定是对象是状态的不可变快照表示,您可以通过简单地创建新值来模拟变化的状态代表新的状态。如果您不再需要旧的,就忘记它,让它被垃圾收集。对象在构造后不需要维护任何不变量,因为它是不可变的:它只需要在构造时强制一次不变量。这可以通过使用代数数据类型的精确数据建模(即“使非法状态无法表示”)来完成,或者使用封装和智能构造函数来防止构造无效值(即“通过构造实现正确性”)。

【讨论】:

  • 我不认为“一次与多次”是副作用的良好定义。在此定义下,幂等效应不会被视为效应。
  • @FyodorSoikin:说得好。我想我在想“、一个或多个”。我改写了一下,但很难找到一个很好的简洁解释我的意思。基本上,我认为“副作用”总是取决于观察的上下文。在纯 State/ST 动作中,modify/modifySTRef 是一个 SE inside 一个常见的 runState/runST(因为重点是后续动作可以看到更改),但不在该上下文之外;同样,putStrLn "hi" 是一个 SE within IO,但纯粹来自安全 Haskell 的 POV,它是一种用于 构造 IO 程序的纯功能元语言。
  • 如果您在不纯的环境中工作,您首先会注意到在很多情况下失去幂等性。我认为这个论点仍然是合理的,即使它并不适用于所有类别的副作用。另一个技术含量较低的解释是副作用引入了时间。在这点上我可能是错的,但我认为结合律是强制 FP 中没有时间的最重要的律。所以副作用会干扰关联性。
【解决方案2】:

副作用可以是对可观察状态的任何更改; “内部”与“外部”函数不是用于此目的的有用或明确定义的限定符。

考虑 C 中的 static 局部变量,它创建了一个可供该函数访问的全局变量。问题是该函数可以从任何地方、任何线程中调用。是否考虑函数“内部”的变量并不重要:如果函数可以读取和更新静态变量,则由于副作用,它不是可重入的。

副作用的危险在于它对程序员是隐藏的。如果程序员清楚可观察到的状态,那么您可以争辩说它“只是一种效果,而不是副作用”。例如,C++ 非常量方法应该能够更新其对象的状态。每次调用该方法时,都需要提供一个目标对象供其更新;这种效果不像静态变量那样危险,因为可观察状态是明显的(而不是“偏向一边”)。但是,由于别名的存在,您仍然会遇到麻烦:例如,如果方法的一个参数恰好是对目标对象的另一个引用...

在某种程度上,“副作用”也与您选择认为重要的内容有关。例如,您可以在调试器中运行纯函数并设置断点。那么该函数是否不那么纯粹,因为它可以在您的屏幕上打印堆栈跟踪?这取决于您是否认为这很重要(在这种情况下,“可能不”)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-12
    • 1970-01-01
    • 2015-08-01
    • 1970-01-01
    相关资源
    最近更新 更多