副作用是任何让您观察基于是否、多少次或的程序行为差异的东西以什么顺序评估表达式或执行操作,即破坏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 中,约定是对象是状态的不可变快照或表示,您可以通过简单地创建新值来模拟变化的状态代表新的状态。如果您不再需要旧的,就忘记它,让它被垃圾收集。对象在构造后不需要维护任何不变量,因为它是不可变的:它只需要在构造时强制一次不变量。这可以通过使用代数数据类型的精确数据建模(即“使非法状态无法表示”)来完成,或者使用封装和智能构造函数来防止构造无效值(即“通过构造实现正确性”)。