问题描述
我试图了解功能性编程和面向对象的编程。我目前试图理解的是面向对象编程中的副作用这一概念,尤其是与增加程序安全性有关的问题。
我将“副作用”理解为会更改链接到对象中一个功能的变量的任何东西,这些变量由对象使用的不同功能使用。但是,如何允许在对象外部而不是在对象内部的函数中设置对象变量。
在我看来,这比在另一个函数中设置它更安全。并且使用该变量的对象中的函数将知道,没有其他函数会在不通知的情况下对其进行更改。
我错过了什么吗?您是否仍认为这是副作用?在对象初始化时设置一堆变量呢?
解决方法
副作用是任何使您能够观察基于 ,多少次或的程序行为差异的方法。以什么顺序计算表达式或执行操作,即破坏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
(但请注意,在评估时,某些IO a
的a
类型的值是纯的:它不是 类型为a
的值“标记”为它来自I / O;相反,它是程序或 action 返回的 连接到a
并由运行时执行时,类型为main
的值。)
请注意,并非所有有效代码都是副作用: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语言中,用于建模变化状态的惯例是创建一个 single 对象,该对象具有身份概念,并随时间使用命令或直接突变对其进行修改。对于每个状态更改,通过维护所有不变量,可以使对象保持有效。
在Haskell中,约定是对象是状态的不可变 snapshot 或 representation ,您可以通过简单地创建一个新值来建模变化的状态代表新的状态。如果您不再需要旧的,只需忘记它,然后将其丢弃即可。构造后,对象无需保持任何不变式,因为它是不可变的:构造完后,只需强制一次 。这可以通过使用代数数据类型(也称为“使非法状态无法表示”)进行精确的数据建模,或者使用封装和智能构造函数来防止构造无效值(也称为“通过构造正确性”)来完成。
,任何副作用都可以改变为可观察状态; “内部”与“外部”功能不是为此目的有用或定义明确的限定符。
请考虑C中的static
局部变量,该变量创建了该函数可访问的全局变量。问题在于该函数可以在任何线程中的任何位置调用。不管您是否认为变量在函数内部都无所谓:如果函数可以读取和更新静态变量,则由于副作用,它不会重入。
副作用的危险在于它对程序员是隐藏的。如果程序员可以观察到可观察的状态,那么您可以说这只是“一种效果,而不是副作用”。例如,期望C ++非常量方法能够更新其对象的状态。每次调用该方法时,都需要提供一个目标对象以进行更新。这种效果不像静态变量那样危险,因为可观察到的状态是明显的(而不是“偏于侧面”)。但是,由于混叠,您仍然会遇到麻烦:例如,如果该方法的参数之一恰好是对目标对象的另一个引用...
在某种程度上,“副作用”也与您选择的重要内容有关。例如,您可以在调试器中运行纯函数,然后设置断点。该函数是否会因为可以在您的屏幕上显示堆栈跟踪信息而变得不那么纯正?这取决于您是否认为这很重要(在这种情况下,“可能不是”)。