根据着名的论文Idioms are oblivious, arrows are meticulous, monads are promiscuous,箭头的表达能力(没有任何额外的类型类)应该在应用函子和monad之间严格地说:monad相当于ArrowApply
和{{1}应该相当于纸张称为“静态箭头”的东西。但是,我不清楚这种“静态”意味着什么限制。
在讨论这三个类型类时,我能够在应用函子和箭头之间建立等价,我在下面的Applicative
和{{1}之间众所周知的等价的上下文中提到了这一点。 }。这种结构是否正确? (在对此感到厌倦之前,我已经证明了大部分arrow laws)。这是否意味着Monad
和ArrowApply
完全相同?
Arrow
答案 0 :(得分:29)
每个应用程序产生一个箭头,每个箭头产生一个应用程序,但它们不等同。如果您有一个箭头arr
和一个态射arr a b
,那么您就不会生成复制其功能的态射arr o (a \to b)
。因此,如果您通过应用程序往返,您将失去一些功能。
申请人是monoidal仿函数。箭头也是类别,或类似地,在算子类别中的幺半群。这两个概念之间没有天然的联系。如果你原谅我的轻浮:在Hask中,事实证明箭头中的pro-functor的仿函数部分是一个幺正的仿函数,但是这种构造必然会忘记" pro"部分。
当你从箭头转到应用程序时,你忽略了一个箭头部分,它接受输入并只使用处理输出的部分。许多有趣的箭头以某种方式使用输入部分,因此通过将它们转换为应用程序,您放弃了有用的东西。
那就是说,在实践中,我发现应用更好的抽象来工作,而且几乎总是做我想要的。从理论上讲,箭头更强大,但我并没有在实践中发现自己使用箭头。
答案 1 :(得分:25)
让我们将IO applicative functor与IO monad的Kleisli箭头进行比较。
您可以使用箭头打印上一个箭头读取的值:
runKleisli ((Kleisli $ \() -> getLine) >>> Kleisli putStrLn) ()
但你不能用applicative functor做到这一点。使用applicative仿函数,所有效果都在之前将函数函数应用于仿函数中的参数。函数中的函数不能使用函数中的参数值来“调整”它自己的效果,可以这么说。
答案 2 :(得分:9)
(我已将以下内容发布到my blog并附带了扩展介绍)
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的monad(例如我们有某种提取器函数的μ α → α
),这种区别变得有点模糊,但考虑尝试仍然有意义在不评估monad影响的情况下提取信息(例如,我们可以问“我们对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
和{{1} }。
Applicarrow 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)
的{{1}}将是:
Applicarrow
当然仍然存在无法撰写IOF
和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
管道本身之前),但我们可以静态地确定将被修改的文件名集。
Monad对静态分析是不透明的,而应用函子在表达动态时间数据依赖性方面很差。事实证明,箭头可以在两者之间提供一个最佳点:通过仔细选择纯粹的功能和箭头化的输入,可以创建一个界面,允许动态行为的正确相互作用和静态分析的适应性。