[英]Arrows are exactly equivalent to applicative functors?
根据著名的论文, 成语是忽略的,箭头是一丝不苟的,单子是混杂的 ,箭头的表达能力(不带任何其他类型类)应严格位于应用函子和单子之间:单子等于ArrowApply
,而Applicative
则等于论文称其为“静态箭头”。 但是,我不清楚这种“静态”状态意味着什么限制。
通过研究三个有问题的类型类,我能够在应用函子和箭头之间建立等价关系,下面在众所周知的Monad
和ArrowApply
等价关系中介绍这些等价关系。 这种结构正确吗? (在无聊之前,我已经证明了大多数箭头法则 )。 这不是说Arrow
和Applicative
完全相同吗?
{-# LANGUAGE TupleSections, NoImplicitPrelude #-}
import Prelude (($), const, uncurry)
-- In the red corner, we have arrows, from the land of * -> * -> *
import Control.Category
import Control.Arrow hiding (Kleisli)
-- In the blue corner, we have applicative functors and monads,
-- the pride of * -> *
import Control.Applicative
import Control.Monad
-- Recall the well-known result that every monad yields an ArrowApply:
newtype Kleisli m a b = Kleisli{ runKleisli :: a -> m b}
instance (Monad m) => Category (Kleisli m) where
id = Kleisli return
Kleisli g . Kleisli f = Kleisli $ g <=< f
instance (Monad m) => Arrow (Kleisli m) where
arr = Kleisli . (return .)
first (Kleisli f) = Kleisli $ \(x, y) -> liftM (,y) (f x)
instance (Monad m) => ArrowApply (Kleisli m) where
app = Kleisli $ \(Kleisli f, x) -> f x
-- Every arrow arr can be turned into an applicative functor
-- for any choice of origin o
newtype Arrplicative arr o a = Arrplicative{ runArrplicative :: arr o a }
instance (Arrow arr) => Functor (Arrplicative arr o) where
fmap f = Arrplicative . (arr f .) . runArrplicative
instance (Arrow arr) => Applicative (Arrplicative arr o) where
pure = Arrplicative . arr . const
Arrplicative af <*> Arrplicative ax = Arrplicative $
arr (uncurry ($)) . (af &&& ax)
-- Arrplicatives over ArrowApply are monads, even
instance (ArrowApply arr) => Monad (Arrplicative arr o) where
return = pure
Arrplicative ax >>= f =
Arrplicative $ (ax >>> arr (runArrplicative . f)) &&& id >>> app
-- Every applicative functor f can be turned into an arrow??
newtype Applicarrow f a b = Applicarrow{ runApplicarrow :: f (a -> b) }
instance (Applicative f) => Category (Applicarrow f) where
id = Applicarrow $ pure id
Applicarrow g . Applicarrow f = Applicarrow $ (.) <$> g <*> f
instance (Applicative f) => Arrow (Applicarrow f) where
arr = Applicarrow . pure
first (Applicarrow f) = Applicarrow $ first <$> f
每个应用程序产生一个箭头,每个箭头产生一个应用程序,但是它们并不等同。 如果您有一个箭头arr
和一个态射arr ab
,那么您将无法生成可以复制其功能的一个态射arr o (a \\to b)
。 因此,如果您遍历应用程序,则会丢失某些功能。
称谓词是单调子函子。 箭头是也属于类别的profunctors,或者等效地,属于类别中的monoid。 这两个概念之间没有自然的联系。 如果您不能原谅我的轻描淡写:在Hask中,箭头中的pro-functor的functor部分是一个单面仿函数,但是这种构造必然会忘记“ pro”部分。
当您从箭头转到应用程序时,您将忽略箭头中需要输入的部分,而仅使用处理输出的部分。 许多有趣的箭头以一种或另一种方式使用输入部分,因此通过将它们转换为应用程序,您就放弃了有用的东西。
就是说,在实践中,我发现可以使用更好的抽象来工作,并且几乎总是可以满足我的要求。 从理论上讲,箭头的功能更强大,但在实践中我并不认为自己会使用它们。
让我们将IO应用函子与IO monad的Kleisli箭头进行比较。
您可以使用箭头来打印上一个箭头读取的值:
runKleisli ((Kleisli $ \() -> getLine) >>> Kleisli putStrLn) ()
但是您不能使用应用函子来做到这一点。 对于应用函子,所有作用都发生在将函数函数应用于函数自变量之前。 可以说,函数中的函数不能使用函数中的参数内部的值“调制”其自身的效果。
(我将以下内容发布到博客中并进行了详细介绍)
汤姆·埃利斯(Tom Ellis)建议考虑一个涉及文件I / O的具体示例,因此让我们比较使用三种类型类的三种方法。 为简单起见,我们只关心两个操作:从文件读取字符串并将字符串写入文件。 文件将通过文件路径来标识:
type FilePath = String
我们的第一个I / O接口定义如下:
data IOM ∷ ⋆ → ⋆
instance Monad IOM
readFile ∷ FilePath → IOM String
writeFile ∷ FilePath → String → IOM ()
例如,使用此接口,我们可以将文件从一个路径复制到另一路径:
copy ∷ FilePath → FilePath → IOM ()
copy from to = readFile from >>= writeFile to
但是,我们可以做的还不止这些:我们操作的文件的选择可能取决于上游的效果。 例如,下面的函数获取一个包含文件名的索引文件,并将其复制到给定的目标目录:
copyIndirect ∷ FilePath → FilePath → IOM ()
copyIndirect index target = do
from ← readFile index
copy from (target ⟨/⟩ to)
另一方面,这意味着无法预先知道将由给定值action ∷ IOM α
操纵的文件名集。 我的意思是写一个纯函数fileNames :: IOM α → [FilePath]
。
当然,对于非基于IO的单子(例如我们具有某种提取函数μ α → α
那个),这种区别变得更加模糊,但是考虑尝试在不使用信息的情况下提取信息仍然有意义。评估单声道的效果(例如,我们可以问“如果没有手头的类型Γ
的值,我们对Reader Γ α
有什么了解?”)。
我们不能在这种情况下真正对monad进行静态分析的原因是,绑定右侧的函数在Haskell函数的空间内,因此完全不透明。
因此,让我们尝试将我们的界面限制为仅一个应用函子。
data IOF ∷ ⋆ → ⋆
instance Applicative IOF
readFile ∷ FilePath → IOF String
writeFile ∷ FilePath → String → IOF ()
由于IOF
不是monad,因此无法组合readFile
和writeFile
,因此我们只能使用该接口读取文件,然后纯粹对其内容进行后处理,或写入文件。 但是无法将文件的内容写入另一个文件。
如何更改writeFile
的类型?
writeFile′ ∷ FilePath → IOF (String → ())
这个介面的主要问题是,虽然它允许编写类似
copy ∷ FilePath → FilePath → IOF ()
copy from to = writeFile′ to ⟨*⟩ readFile from
这会导致各种麻烦的问题,因为String → ()
是一种将字符串写入文件的可怕模型,因为它破坏了参照透明性。 例如,运行该程序后,您期望out.txt
的内容是什么?
(λ write → [write "foo", write "bar", write "foo"]) ⟨$⟩ writeFile′ "out.txt"
首先,让我们获得两个基于箭头的I / O接口,它们不会(实际上,无法)为表带来新的东西: Kleisli IOM
和Applicarrow IOF
。
IOM
的Kleisli-arrow模运算为:
readFile ∷ Kleisli IOM FilePath String
writeFile ∷ Kleisli IOM (FilePath, String) ()
由于writeFile
的输入仍然包含文件名和内容,因此我们仍然可以编写copyIndirect
(为简单起见,使用箭头表示法)。 请注意,甚至没有使用Kleisli IOM
的ArrowApply
实例。
copyIndirect ∷ Kleisli IOM (FilePath, FilePath) ()
copyIndirect = proc (index, target) → do
from ← readFile ↢ index
s ← readFile ↢ from
writeFile ↢ (to, s)
IOF
的Applicarrow
范围是:
readFile ∷ FilePath → Applicarrow IOF () String
writeFile ∷ FilePath → String → Applicarrow IOF () ()
当然,哪一个仍然表现出无法组成readFile
和writeFile
的相同问题。
如果我们从头开始,然后尝试在使用Haskell函数的位置和制作箭头的位置之间尝试创建一些东西,而不是将IOM
或IOF
转换为箭头,该怎么办? 使用以下界面:
data IOA ∷ ⋆ → ⋆ → ⋆
instance Arrow IOA
readFile ∷ FilePath → IOA () String
writeFile ∷ FilePath → IOA String ()
因为writeFile
从箭头的输入端获取内容,所以我们仍然可以实现copy
:
copy ∷ FilePath → FilePath → IOA () ()
copy from to = readFile from >>> writeFile to
但是, writeFile
的另一个参数是一个纯函数性的参数,因此它不能依赖于readFile
的输出。 因此copyIndirect
不能使用此 Arrow接口实现。
如果我们扭转这个论点,这也意味着虽然我们无法事先知道将最终写入文件的内容(在运行完整的IOA
管道本身之前),但是我们可以静态地确定将要写入的文件名集。改性。
Monad对静态分析是不透明的,而应用函子在表达动态时间数据依赖性方面很差。 事实证明,箭头可以提供两者之间的最佳结合:通过仔细选择纯功能的和箭头化的输入,可以创建一个界面,该界面仅允许动态行为与静态分析的正确交互。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.