Evg*_*Evg 1 io monads haskell functional-programming monoids
在 haskell IO 类型中有 Monoid 的实例:
instance Monoid a => Monoid (IO a) where
mempty = pure empty
Run Code Online (Sandbox Code Playgroud)
如果我有三个共享某个状态的操作,并通过副作用改变彼此的行为,从 IO 类型的角度来看,这可能会导致违反关联律:
a1:: IO String
a2:: IO String
a3:: IO String
Run Code Online (Sandbox Code Playgroud)
(a1 mappenda2) mappenda3 /= a1 mappend(a2 mappenda3)
例如,如果 a1,a2,a3 请求字符串中的当前时间,或者 IO 包含一些计算请求编号的 DB。这意味着它可以是:
(a1 `mappend` a2) `mappend` a3 == "1"++"2"++"3"
a1 `mappend` (a2 `mappend` a3) == "3"++"1"++"2"
Run Code Online (Sandbox Code Playgroud)
编辑:
我想我不应该用 db 举一个例子,它很困惑,更喜欢的例子:
a1 = show <$> getUnixTime
a2 = show <$> getUnixTime
a3 = show <$> getUnixTime
l = (a1 `mappend` a2) `mappend` a3
r = a1 `mappend` (a2 `mappend` a3)
liftA2 (==) l r
**False**
Run Code Online (Sandbox Code Playgroud)
那么为什么 IO 类型是幺半群,如果它可以打破结合律呢?或者我错过了什么?
Wil*_*sem 12
a1 `mappend` (a2 `mappend` a3)不按顺序运行a2,a3并且a1。相反如Python当务之急语言例如,在Haskell一个IO a不是一些结果的计算的,它是一种配方,以产生的值a。您实际上可以看到IO更像是 Python 中的延续,您传递一个函数,以便最终可以调用它,但您不直接调用它。
正如我们在源代码中看到的mappend那样liftA2 (<>),该函数是针对Semigroup a => Semigroup (IO a)实例实现的:
Run Code Online (Sandbox Code Playgroud)instance Semigroup a => Semigroup (IO a) where (<>) = liftA2 (<>)
因此,这意味着mappend实现为:
mappendIO :: Semigroup a => IO a -> IO a -> IO a
mappendIO f g = do
x <- f
y <- g
pure (x <> y)Run Code Online (Sandbox Code Playgroud)
所以它f在g.
如果我们现在看(a1 `mappend` a2) `mappend` a3,我们会看到:
(a1 `mappend` a2) `mappend` a3 = do
x <- do
x1 <- a1
x2 <- a2
pure (x1 <> x2)
y <- a3
pure (x <> y)Run Code Online (Sandbox Code Playgroud)
这相当于:
(a1 `mappend` a2) `mappend` a3 = do
x1 <- a1
x2 <- a2
x3 <- a3
pure ((x1 <> x2) <> x3)Run Code Online (Sandbox Code Playgroud)
如果我们再看一下,a1 `mappend` (a2 `mappend` a3)这相当于:
a1 `mappend` (a2 `mappend` a3) = do
x <- a1
y <- do
y1 <- a2
y2 <- a2
pure (y1 <> y2)
pure (x <> y)Run Code Online (Sandbox Code Playgroud)
这相当于:
a1 `mappend` (a2 `mappend` a3) = do
x1 <- a1
x2 <- a2
x3 <- a2
pure (x1 <> (x2 <> x3))Run Code Online (Sandbox Code Playgroud)
由于x1 <> (x2 <> x3)等效于(x1 <> x2) <> x3,因此这将在两个项目中返回相同的结果。
至于你的测试:
l = (a1 `mappend` a2) `mappend` a3
r = a1 `mappend` (a2 `mappend` a3)
liftA2 (==) l r
False
Run Code Online (Sandbox Code Playgroud)
请注意,liftA2 (==)再次将定义一个序列,因此这意味着您liftA2 (==) l r的定义为:
liftA2 (==) l r = do
x1 <- a1
x2 <- a2
x3 <- a3
y1 <- a1
y2 <- a2
y3 <- a3
pure ((x1 <> x2) <> x3) == (y1 <> (y2 <> y3))
Run Code Online (Sandbox Code Playgroud)
你因此r 追赶 l。
如果您使用Statemonad,您可以更清楚地说明会发生什么,并验证是否应用了规则。您需要在和之间重置状态。lr
您不能liftA2 (==)用来有意义地比较 IO 值:这种比较关系甚至不是自反的!
事实上,如果我们运行
a1 = show <$> getUnixTime
liftA2 (==) a1 a1
Run Code Online (Sandbox Code Playgroud)
可能会得到一个False结果,因为在两次调用之间经过了时间getUnixTime,所以返回的值可能不同。
如果您定义a1返回某个随机数生成器的值,这一点就更加清楚了。调用两次几乎总是会产生不同的结果。
另一个例子:liftA2 (==) getLine getLine如果用户输入两个不同的行,则可以返回 false。
当我们说它ioAction1等于时,ioAction2我们的意思是如果在完全相同的上下文中执行它们将具有相同的效果。这是不一样的,作为执行一个动作后,其他并比较结果。
但是,精确定义“相同的 IO 效果”很棘手,因为我们通常希望忽略性能差异。例如return () >> print True可能比 稍慢print True,如果不执行优化,我们仍然希望将这两个操作视为具有相同的效果。