我正在阅读 Haskell 背后的数学基础——我已经了解了如何使用闭包来保存函数中的状态。
我想知道 Haskell 是否允许闭包,以及它们是如何工作的,因为它们不是纯函数?
如果一个函数修改了它的关闭状态,它将能够在相同的输入上给出不同的输出。
这在 Haskell 中怎么不是问题?是因为在最初为变量赋值后无法重新分配变量吗?
我正在阅读 Haskell 背后的数学基础——我已经了解了如何使用闭包来保存函数中的状态。
我想知道 Haskell 是否允许闭包,以及它们是如何工作的,因为它们不是纯函数?
如果一个函数修改了它的关闭状态,它将能够在相同的输入上给出不同的输出。
这在 Haskell 中怎么不是问题?是因为在最初为变量赋值后无法重新分配变量吗?
你实际上可以在 Haskell 中模拟闭包,但不是你想象的那样。首先,我将定义一个闭包类型:
data Closure i o = Respond (i -> (o, Closure i o ))
这定义了一个类型,在每个“步骤”中都采用一个 type 的值,i
用于计算 type 的响应o
。
所以,让我们定义一个“闭包”,它接受空输入和整数回答,即:
incrementer :: Closure () Int
这个闭包的行为会因请求而异。我将保持简单并使其以 0 响应第一个响应,然后为每个连续的请求增加其响应:
incrementer = go 0 where
go n = Respond $ \() -> (n, go (n + 1))
然后我们可以重复查询闭包,这会产生一个结果和一个新的闭包:
query :: i -> Closure i o -> (o, Closure i o)
query i (Respond f) = f i
请注意,上述类型的后半部分类似于 Haskell 中的常见模式,即State
monad:
newtype State s a = State { runState :: s -> (a, s) }
它可以从Control.Monad.State
. 所以我们可以包装query
在这个State
monad 中:
query :: i -> State (Closure i o) o
query i = state $ \(Respond f) -> f i
...现在我们有了一种通用的方法来使用State
monad 查询任何闭包:
someQuery :: State (Closure () Int) (Int, Int)
someQuery = do
n1 <- query ()
n2 <- query ()
return (n1, n2)
让我们将闭包传递给它,看看会发生什么:
>>> evalState someQuery incrementer
(0, 1)
让我们编写一个不同的闭包,它返回一些任意模式:
weirdClosure :: Closure () Int
weirdClosure = Respond (\() -> (42, Respond (\() -> (666, weirdClosure))))
...并测试它:
>>> evalState someQuery weirdClosure
(42, 666)
现在,手动编写闭包似乎很尴尬。如果我们可以使用do
符号来编写闭包不是很好吗?好吧,我们可以!我们只需要对我们的闭包类型进行一次更改:
data Closure i o r = Done r | Respond (i -> (o, Closure i o r))
现在我们可以为 定义一个Monad
实例(来自Control.Monad
)Closure i o
:
instance Monad (Closure i o) where
return = Done
(Done r) >>= f = f r
(Respond k) >>= f = Respond $ \i -> let (o, c) = k i in (o, c >>= f)
我们可以编写一个对应于服务单个请求的便利函数:
answer :: (i -> o) -> Closure i o ()
answer f = Respond $ \i -> (f i, Done ())
...我们可以用它来重写我们所有的旧闭包:
incrementer :: Closure () Int ()
incrementer = forM_ [1..] $ \n -> answer (\() -> n)
weirdClosure :: Closure () Int r
weirdClosure = forever $ do
answer (\() -> 42)
answer (\() -> 666)
现在我们只需将查询函数更改为:
query :: i -> StateT (Closure i o r) (Either r) o
query i = StateT $ \x -> case x of
Respond f -> Right (f i)
Done r -> Left r
...并使用它来编写查询:
someQuery :: StateT (Closure () Int ()) (Either ()) (Int, Int)
someQuery = do
n1 <- query ()
n2 <- query ()
return (n1, n2)
现在测试一下!
>>> evalStateT someQuery incrementer
Right (1, 2)
>>> evalStateT someQuery weirdClosure
Right (42, 666)
>>> evalStateT someQuery (return ())
Left ()
但是,我仍然不认为这是一种真正优雅的方法,因此我将无耻地将我的代理类型插入我的Proxy类型pipes
作为编写闭包及其消费者的更通用和更结构化的方式来结束。Server
类型表示一个广义闭包,而表示Client
一个闭包的广义使用者。
闭包只是为函数“添加”了额外的变量,因此与使用“正常”变量相比,您对它们无能为力,也就是说,当然不会修改状态。
阅读更多: 闭包(在 Haskell 中)
正如其他人所说,Haskell 不允许更改闭包中的“状态”。这可以防止你做任何可能破坏函数纯度的事情。