繁体   English   中英

箭头完全等于应用函子吗?

[英]Arrows are exactly equivalent to applicative functors?

根据著名的论文, 成语是忽略的,箭头是一丝不苟的,单子是混杂的 ,箭头的表达能力(不带任何其他类型类)应严格位于应用函子和单子之间:单子等于ArrowApply ,而Applicative则等于论文称其为“静态箭头”。 但是,我不清楚这种“静态”状态意味着什么限制。

通过研究三个有问题的类型类,我能够在应用函子和箭头之间建立等价关系,下面在众所周知的MonadArrowApply等价关系中介绍这些等价关系。 这种结构正确吗? (在无聊之前,我已经证明了大多数箭头法则 )。 这不是说ArrowApplicative完全相同吗?

{-# 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

我们的第一个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函数的空间内,因此完全不透明。

因此,让我们尝试将我们的界面限制为仅一个应用函子。

适用的I / O

data IOF ∷ ⋆ → ⋆
instance Applicative IOF
readFile ∷ FilePath → IOF String
writeFile ∷ FilePath → String → IOF ()

由于IOF不是monad,因此无法组合readFilewriteFile ,因此我们只能使用该接口读取文件,然后纯粹对其内容进行后处理,或写入文件。 但是无法将文件的内容写入另一个文件。

如何更改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的两种方法

首先,让我们获得两个基于箭头的I / O接口,它们不会(实际上,无法)为表带来新的东西: Kleisli IOMApplicarrow IOF

IOM的Kleisli-arrow模运算为:

readFile ∷ Kleisli IOM FilePath String
writeFile ∷ Kleisli IOM (FilePath, String) ()

由于writeFile的输入仍然包含文件名和内容,因此我们仍然可以编写copyIndirect (为简单起见,使用箭头表示法)。 请注意,甚至没有使用Kleisli IOMArrowApply实例。

copyIndirect ∷ Kleisli IOM (FilePath, FilePath) ()
copyIndirect = proc (index, target) → do
    from ← readFile ↢ index
    s ← readFile ↢ from
    writeFile ↢ (to, s)

IOFApplicarrow范围是:

readFile ∷ FilePath → Applicarrow IOF () String
writeFile ∷ FilePath → String → Applicarrow IOF () ()

当然,哪一个仍然表现出无法组成readFilewriteFile的相同问题。

正确的带箭头的I / O接口

如果我们从头开始,然后尝试在使用Haskell函数的位置和制作箭头的位置之间尝试创建一些东西,而不是将IOMIOF转换为箭头,该怎么办? 使用以下界面:

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM