monad的例子,其应用部分可以比Monad部分更好地优化

时间:2013-09-03 11:02:46

标签: performance haskell monads applicative

在一次讨论中,我听说一些解析器的Applicative接口的实现方式不同,比Monad接口更有效。原因是,在整个有效计算运行之前,Applicative我们事先知道所有“效果”。对于monad,效果可能取决于计算过程中的值,因此无法进行此优化。

我想看到一些很好的例子。它可以是一些非常简单的解析器或一些不同的monad,这并不重要。重要的是,此类monad的Applicative接口符合其returnap,但使用Applicative可生成更高效的代码。

更新:只是为了澄清,在这里我对那些不能成为monad的应用程序不感兴趣。问题是关于两者的事情。

3 个答案:

答案 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<*>取代。但>>=只能在每次函数调用后分配适量的内存,然后将结果放到适当位置,这看起来非常耗时;而<*>可以简单地将结果长度预先计算为functionsvalues长度的乘积,然后写入一个固定数组。