[译] 函数式 Arrow 实用案例

题叶
原文 https://tuttlem.github.io/201...

介绍

Arrow 提了一种表示"计算"的手段. 甚至有些 Monad 用法里边看不到的复杂操作, 通过 Arrow 提供的一些有意思的组合式的组合方法, 也可以构造出来.

Arrow ClassControl.Arrows 里定义, 从基础库可以引入.
根据 Haskell 官网的文档表示:

Arrows are a new abstract view of computation, defined by John Hughes [Hug00]. They serve much the same purpose as monads – providing a common structure for libraries – but are more general. In particular they allow notions of computation that may be partially static (independent of the input) or may take multiple inputs. If your application works fine with monads, you might as well stick with them. But if you’re using a structure that’s very like a monad, but isn’t one, maybe it’s an arrow.

Arrow 是 John Hughes 定义的一种对于"计算过程"的抽象表示. 其功能与 Monad 相似 - 为各个类库提供一个常用结构 - 甚至更加通用. 特别是 Arrow 允许计算的表达可以部分地作为静态(跟输入无关), 或者接受多个输入. 如果一个应用用 Monad 写没问题, 那你可以继续沿着 Monad. 不过如果用到一个结构非常像 Monad, 但又不是 Monad,, 那有可能就是 Arrow 了.

如果你对于学习 Arrow 背后的理论感兴趣, 或者想要深入了解, 我强烈推荐你把上边几个链接彻底读一读.

这个文章的内容是带着你看看 Haskell 里边 Arrow 有哪些实际的例子.

returnA

returnA 提供 identity Arrow. 对应 Monadic 上下文档中原来你用 return 的地方, 用这个代替.

λ> :t returnA
returnA :: Arrow a => a b b
λ> returnA 5
5
λ> :t returnA 5
returnA 5 :: Num b => b

arr

arr 接受一个普通函数, 然后返回一个构造的 Arrow:

λ> let a1 = arr (+)
λ> :t a1
a1 :: (Arrow a, Num b) => a b (b -> b)

然后调用 Arrow 可以直接用:

λ> a1 (returnA 5) 6
11
λ> a1 5 6
11

>>>

>>> 对两个 Arrow 进行从左到右的组合. 这个和 Monad 提供的 >>= 非常相似.

λ> let a1 = arr (+5)
λ> let a2 = arr (*2)
λ> a1 >>> a2 $ 3
16

后面文章罗列的几个函数用于元组(Tuple)结构.
这是 Arrow 相对于 Monad, 在组合能力方面产生差别的地方.
作为例子, 先定一个整数的元组:

λ> let q = (1,2)

first

first 对元组的第一个元素调用 Arrow,
而第二个值不发生改变.

λ> first a1 q
(6,2)

second

second 对元组第二个值调用, 第一个不改变.

λ> let q = (1,2)

λ> second a1 q
(1,7)

***

*** 对元组第一个元素调用第一个 Arrow, 对第二个元素调用第二个 Arrow.
后面这个例子, 1a1 调用, 2a2 调用.

λ> let q = (1,2)

λ> a1 *** a2 $ q
(6,4)

&&&

&&& 会把一个值变成一个元组, 然后分别进行调用, 最后返回元组.

λ> let a1 = arr (+3)
λ> let a2 = arr (+30)
λ> 1 &&& a2 $ 3
(6,33)

proc

proc(Arrow 抽象) 通过 Arrow 而不是普通函数来构造一个 lambda 函数.
proc 允许你用 Arrow Notation来建构表达式(或者 Arrow).

{-# Language Arrows #-}

import Control.Arrow

addA :: Arrow a => a b Int -> a b Int -> a b Int
addA f g = proc x -> do
            y <- f -< x
            z <- g -< x
            returnA -< y + z

该例中, addA 的类型签名接受两个从 bInt(a b Int)的 Arrow, 返回一个同类型的 Arrow.

proc 代码块有一个 lambda 变量 x, 在 Arrow 调用时被应用.
注意, 这里只是构造了一个 Arrow, 而不是现在就运行它.

后续代码用了一个新的操作符 -<(Arrow Application), 用于将表达式的值传给 Arrow.
可能这样好理解一点:

"绑定了变量的表达式" <- "Arrow" <- "作为 Arrow 的输入项的纯表达式"

调用 addA 是这样子:

λ> let a1 = arr (+4)
λ> addA a1 a1 (returnA 3)
14

这其中的 arrow a1 表现就像是 (+4) 一样. a1` 用在第一个和第二个参数.
最后我们把纯的值 returnA 3(就是 3)传给 addA.

所以这里可以看到 (+4) 应用到了 3, 另一个位置也是 (+4) 应用到 3, 对的, 发生了两次.
这是因为里边两个参数都用了 a1.
最后的结果是两次 Arrow 的调用结果再被相加, 得到 14.
纯的数值是通过 returnA 3 提供的.

这就是一些关于 Arrow 的非常基础的用例.
前面提供的链接当中能看到更多, 可以了解到 Arrow 是多么强大的结构.


译注

文章就是个简单 Arrow 的介绍, 链接里边的就非常难懂了.
大致可以看到, 简单场景 Arrow 相当于一个抽象,
普通函数就是一种 Arrow, 而 Arrow 的概念上就是抽象化的这样一种计算过程.
前面例子当中, arr (+) 可以直接当做 (+) 使用,
看下相似的函数的类型签名:

Prelude Control.Arrow> :t (\x -> x + 1)
(\x -> x + 1) :: Num a => a -> a
Prelude Control.Arrow> let a1 = arr (\x -> x + 1)
Prelude Control.Arrow> :t a1
a1 :: (Arrow a, Num c) => a c c

其中:

(\x -> x + 1).    :: Num a            => a -> a
arr (\x -> x + 1) :: (Arrow a, Num c) => a c c

这里的 a -> aa c c, 或者 Num -> NumArrow Num Num, 对应,
-> 也是一种 Arrow 的实例.
这样大致可以理解 Arrow 相对于普通函数来说是怎样一种抽象吧.

另外文章后面的例子, 详细一点就是:

{-# Language Arrows #-}

import Control.Arrow

addA :: Arrow a => a b Int -> a b Int -> a b (Int, Int)
addA f g = proc x -> do
            y <- f -< x
            z <- g -< x
            returnA -< (y, z)


a1 = arr (+4)
a2 = arr (+40)


main = do
  putStrLn $ show $ addA a1 a2 3
=>> runghc demo.hs
(7,43)

可以看到这个静态的过程具体的例子了.
至于再强大的一点的例子, 还得再扒别的文章看看...

阅读 105

题叶
ClojureScript 爱好者.

ClojureScript 爱好者.

17.2k 声望
2.6k 粉丝
0 条评论
你知道吗?

ClojureScript 爱好者.

17.2k 声望
2.6k 粉丝
宣传栏