mcz*_*nek 1 haskell functional-programming side-effects
我试图理解函数式和面向对象的编程。我目前试图理解的是面向对象编程中副作用的概念,特别是当它与提高程序的安全性有关时。
我将“副作用”理解为任何改变链接到一个对象内的一个函数的变量的任何东西,这些变量被一个对象使用的不同函数使用。但是如果允许在对象外部设置对象变量,但不允许在对象内部的函数内设置呢?
在我看来,这比将它设置在另一个函数中更安全。并且该对象中使用此变量的函数会知道没有其他函数会在不通知的情况下更改它。
我错过了什么吗?你还会认为这是副作用吗?在对象初始化时设置一堆变量怎么样?
副作用是任何可以让您根据是否、多少次或以何种顺序计算表达式或执行操作来观察程序行为差异的任何东西,即破坏引用透明性。变异变量是副作用的一个例子,但在并发通道上发送消息、打印到终端、写入文件或从网络读取也是如此。
正常安全代码中的可观察性使某些事情产生副作用;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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
(但要注意类型的值IO a一些a 是当纯评估:这是不是类型的值a“标记”,它来自于I / O的事实;相反,它是一个程序或操作该返回类型的值a时大呼过瘾达main和由运行时执行。)
请注意,并非所有有效的代码都有副作用:pure () :: IO ()存在IO但显然没有副作用。同样,ST提供保证不会转义或在其范围之外可见的局部可变变量,因此您可以实现内部不纯的纯函数:
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
Run Code Online (Sandbox Code Playgroud)
至于“在对象初始化时设置一堆变量”,这基本上是 Haskell 中使用的模式,不仅有助于加强安全性,而且作为一种受数学启发的数据建模哲学。
在 OOP 语言中,对更改状态进行建模的约定是创建一个具有标识概念的单个对象,并使用命令或直接更改随时间对其进行修改。通过为每个状态更改维护其所有不变量,期望对象保持有效。
而在 Haskell 中,约定是对象是不可变的快照或状态的表示,您可以通过简单地创建一个新值来表示新状态来对不断变化的状态进行建模。如果您不再需要旧的,只需忘记它并让它被垃圾收集。对象在构造后不需要维护任何不变量,因为它是不可变的:它只需要在构造时强制执行一次不变量。这可以通过使用代数数据类型的精确数据建模(又名“使非法状态不可表示”)或使用封装和智能构造函数来防止构造无效值(又名“构造正确性”)来完成。