37

根据著名论文Idioms are oblivious, arrows are meticulous, monads are promiscuous,箭头的表达能力(没有任何额外的类型类)应该严格介于 applicative functors 和 monads 之间:monads 等价于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
4

3 回答 3

32

每个应用程序都会产生一个箭头,每个箭头都会产生一个应用程序,但它们并不等价。如果你有一个箭头arr和一个态射arr a b,它并不意味着你可以生成一个arr o (a \to b)复制其功能的态射。因此,如果您通过应用程序往返,您将失去一些功能。

应用程序是单曲面函子。箭头是profunctors,也是类别,或者等效地,在profunctors类别中的幺半群。这两个概念之间没有自然的联系。如果你能原谅我的轻率:在 Hask 中,箭头中的 pro-functor 的 functor 部分是幺半群 functor,但这种构造必然会忘记“pro”部分。

当您从箭头转到应用程序时,您会忽略箭头中接受输入的部分,而只使用处理输出的部分。许多有趣的箭头以一种或另一种方式使用输入部分,因此通过将它们变成应用程序,您放弃了有用的东西。

也就是说,在实践中,我发现可以使用更好的抽象,并且几乎总是可以做我想要的。理论上箭头更强大,但我没有发现自己在实践中使用它们。

于 2014-07-10T04:30:48.230 回答
27

让我们将 IO 应用函子与 IO monad 的 Kleisli 箭头进行比较。

您可以有一个箭头来打印由前一个箭头读取的值:

runKleisli ((Kleisli $ \() -> getLine) >>> Kleisli putStrLn) ()

但是你不能用应用函子来做到这一点。使用 applicative functors,所有效果都发生将 function-in-the-functor 应用到 arguments-in-the-functor 之前。可以说,函子中的函数不能使用函子中的参数内的值来“调制”它自己的效果。

于 2014-07-10T06:08:42.190 回答
11

(我已将以下内容发布到我的博客中,并附有详细介绍)

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 的 monad(例如我们有某种提取器函数的 monad μ α → α),这种区别会变得有点模糊,但考虑在不评估效果的情况下尝试提取信息仍然是有意义的。 monad(例如,我们可以问“如果手头Reader Γ α没有 type 的值,我们能知道什么Γ?”)。

我们不能真正对 monad 进行这种意义上的静态分析的原因是绑定右侧的函数位于 Haskell 函数的空间中,因此是完全不透明的。

所以让我们尝试将我们的接口限制为一个应用函子。

应用 I/O

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

由于IOF不是 monad,所以无法组合readFileand 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 的两种方法

首先,让我们摆脱两个基于箭头的 I/O 接口,它们不会(事实上,不能)给桌面带来任何新的东西:Kleisli IOMApplicarrow IOF.

的 Kleisli 箭头IOM,模柯里化,是:

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

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

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

ApplicarrowIOF是:

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

这当然仍然表现出无法撰写readFilewriteFile.

正确的箭头 I/O 接口

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

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是纯函数参数,因此它不能依赖于 eg 的输出readFile;所以copyIndirect不能用这个箭头接口来实现。

如果我们扭转这个论点,这也意味着虽然我们无法提前知道最终将写入文件的内容(在运行完整IOA管道本身之前),但我们可以静态确定将被修改的文件名集.

结论

单子对静态分析是不透明的,应用函子在表达动态时间数据依赖性方面很差。事实证明,箭头可以提供两者之间的最佳点:通过仔细选择纯功能输入和箭头化输入,可以创建一个界面,该界面允许动态行为和静态分析的顺从性之间的正确相互作用。

于 2014-07-12T08:57:28.497 回答