在一次讨论中,我听说一些解析器的Applicative
接口的实现方式不同,比Monad
接口更有效。原因是,在整个有效计算运行之前,Applicative
我们事先知道所有“效果”。对于monad,效果可能取决于计算过程中的值,因此无法进行此优化。
我想看到一些很好的例子。它可以是一些非常简单的解析器或一些不同的monad,这并不重要。重要的是,此类monad的Applicative
接口符合其return
和ap
,但使用Applicative
可生成更高效的代码。
更新:只是为了澄清,在这里我对那些不能成为monad的应用程序不感兴趣。问题是关于两者的事情。
答案 0 :(得分:19)
另一个例子是严格的左折。您可以编写一个应用实例,它允许您组合折叠,以便可以在单个通道和常量空间中对数据执行生成的折叠。但是,monad实例需要从每个绑定的数据开头重新迭代,并将整个列表保留在内存中。
{-# LANGUAGE GADTs #-}
import Criterion.Main
import Data.Monoid
import Control.Applicative
import Control.Monad
import Prelude hiding (sum)
data Fold e r where
Step :: !(a -> e -> a) -> !a -> !(a -> r) -> Fold e r
Bind :: !(Fold e r) -> !(r -> Fold e s) -> Fold e s
data P a b = P !a !b
instance Functor (Fold e) where
fmap f (Step step acc ret) = Step step acc (f . ret)
fmap f (Bind fld g) = Bind fld (fmap f . g)
instance Applicative (Fold e) where
pure a = Step const a id
Step fstep facc fret <*> Step xstep xacc xret = Step step acc ret where
step (P fa xa) e = P (fstep fa e) (xstep xa e)
acc = P facc xacc
ret (P fa xa) = (fret fa) (xret xa)
Bind fld g <*> fldx = Bind fld ((<*> fldx) . g)
fldf <*> Bind fld g = Bind fld ((fldf <*>) . g)
instance Monad (Fold e) where
return = pure
(>>=) = Bind
fold :: Fold e r -> [e] -> r
fold (Step _ acc ret) [] = ret acc
fold (Step step acc ret) (x:xs) = fold (Step step (step acc x) ret) xs
fold (Bind fld g) lst = fold (g $ fold fld lst) lst
monoidalFold :: Monoid m => (e -> m) -> (m -> r) -> Fold e r
monoidalFold f g = Step (\a -> mappend a . f) mempty g
count :: Num n => Fold e n
count = monoidalFold (const (Sum 1)) getSum
sum :: Num n => Fold n n
sum = monoidalFold Sum getSum
avgA :: Fold Double Double
avgA = liftA2 (/) sum count
avgM :: Fold Double Double
avgM = liftM2 (/) sum count
main :: IO ()
main = defaultMain
[ bench "Monadic" $ nf (test avgM) 1000000
, bench "Applicative" $ nf (test avgA) 1000000
] where test f n = fold f [1..n]
我从头顶上写了上面的例子,所以它可能不是应用和monadic折叠的最佳实现,但运行上面给我:
benchmarking Monadic
mean: 119.3114 ms, lb 118.8383 ms, ub 120.2822 ms, ci 0.950
std dev: 3.339376 ms, lb 2.012613 ms, ub 6.215090 ms, ci 0.950
benchmarking Applicative
mean: 51.95634 ms, lb 51.81261 ms, ub 52.15113 ms, ci 0.950
std dev: 850.1623 us, lb 667.6838 us, ub 1.127035 ms, ci 0.950
答案 1 :(得分:17)
也许这个规范的例子是由向量给出的。
data Nat = Z | S Nat deriving (Show, Eq, Ord)
data Vec :: Nat -> * -> * where
V0 :: Vec Z x
(:>) :: x -> Vec n x -> Vec (S n) x
我们可以稍加努力地使它们适用,首先定义单身,然后将它们包装在一个类中。
data Natty :: Nat -> * where
Zy :: Natty Z
Sy :: Natty n -> Natty (S n)
class NATTY (n :: Nat) where
natty :: Natty n
instance NATTY Z where
natty = Zy
instance NATTY n => NATTY (S n) where
natty = Sy natty
现在我们可以开发Applicative
结构
instance NATTY n => Applicative (Vec n) where
pure = vcopies natty
(<*>) = vapp
vcopies :: forall n x. Natty n -> x -> Vec n x
vcopies Zy x = V0
vcopies (Sy n) x = x :> vcopies n x
vapp :: forall n s t. Vec n (s -> t) -> Vec n s -> Vec n t
vapp V0 V0 = V0
vapp (f :> fs) (s :> ss) = f s :> vapp fs ss
我省略Functor
个实例(应该从fmapDefault
实例通过Traversable
提取。)
现在,有一个Monad
实例与此Applicative
对应,但它是什么? 对角思考!这就是所需要的!向量可以看作来自有限域的函数列表,因此Applicative
只是K和S组合子的列表,{{1}有Monad
之类的行为。
Reader
您可以通过更直接地定义vtail :: forall n x. Vec (S n) x -> Vec n x
vtail (x :> xs) = xs
vjoin :: forall n x. Natty n -> Vec n (Vec n x) -> Vec n x
vjoin Zy _ = V0
vjoin (Sy n) ((x :> _) :> xxss) = x :> vjoin n (fmap vtail xxss)
instance NATTY n => Monad (Vec n) where
return = vcopies natty
xs >>= f = vjoin natty (fmap f xs)
来节省一点,但无论如何,monadic行为会为非对角线计算创建无用的thunk。懒惰可能会使我们免于被一个世界末日因素放慢速度,但>>=
的拉链行为必然至少比采用矩阵的对角线便宜一些。
答案 2 :(得分:14)
正如Pigworker所说,数组是明显的例子;他们的monad实例不仅在类型索引长度等概念层面上有点问题,而且在非常真实的Data.Vector
实现中表现更差:
import Criterion.Main
import Data.Vector as V
import Control.Monad
import Control.Applicative
functions :: V.Vector (Int -> Int)
functions = V.fromList [(+1), (*2), (subtract 1), \x -> x*x]
values :: V.Vector Int
values = V.enumFromN 1 32
type NRuns = Int
apBencher :: (V.Vector (Int -> Int) -> V.Vector Int -> V.Vector Int)
-> NRuns -> Int
apBencher ap' = run values
where run arr 0 = V.sum arr
run arr n = run (functions `ap'` arr) $ n-1
main = defaultMain
[ bench "Monadic" $ nf (apBencher ap ) 4
, bench "Applicative" $ nf (apBencher (<*>)) 4 ]
$ ghc-7.6 -O1 -o -fllvm -o bin / bench-d0 def0.hs
$ bench-d0
热身 估计时钟分辨率...
平均值是1.516271 us(640001次迭代)
在639999个样本中发现3768个异常值(0.6%)
2924(0.5%)高严重
估计时钟呼叫的成本...
平均值为41.62906 ns(12次迭代)
在12个样本中发现1个异常值(8.3%)
1(8.3%)高严重
基准Monadic
平均值:2.773062 ms,lb 2.769786 ms,ub 2.779151 ms,ci 0.950
std dev:22.14540 us,lb 13.55686 us,ub 36.88265 us,ci 0.950
基准应用
平均值:1.269351 ms,磅1.267654 ms,ub 1.271526 ms,ci 0.950
std dev:9.799454 us,lb 8.171284 us,ub 13.09267 us,ci 0.950
请注意,使用-O2
进行编译时,性能差异不大;显然,ap
被<*>
取代。但>>=
只能在每次函数调用后分配适量的内存,然后将结果放到适当位置,这看起来非常耗时;而<*>
可以简单地将结果长度预先计算为functions
和values
长度的乘积,然后写入一个固定数组。