Подтвердить что ты не робот

Примеры монады, чья аппликативная часть может быть лучше оптимизирована, чем часть Монады

В одном из сообщений я слышал, что интерфейс Applicative некоторых парсеров реализован по-разному, более эффективно, чем их интерфейс Monad. Причина в том, что при Applicative мы знаем все "эффекты" заранее, прежде чем начнется все эффективное вычисление. С помощью монадов эффекты могут зависеть от значений во время вычисления, поэтому эта оптимизация невозможна.

Я бы хотел увидеть несколько хороших примеров этого. Это может быть очень простой парсер или какая-то другая монада, что не важно. Важно то, что интерфейс Applicative такой монады соответствует ее return и ap, но с помощью Applicative создается более эффективный код.

Обновление: Просто чтобы уточнить, здесь меня не интересуют аппликации, которые не могут быть монадами. Вопрос в том, что они оба.

4b9b3361

Ответ 1

Другим примером является строгая левая складка. Вы можете написать аппликативный экземпляр, который позволяет создавать складки так, чтобы получившаяся сводка могла выполняться на данных за один проход и постоянное пространство. Тем не менее, экземпляр 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]

Я написал выше из верхней части головы в качестве примера, так что это может быть не оптимальная реализация аппликативных и монадических складок, но выполнение приведенного выше дает мне:

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

Ответ 2

Возможно, канонический пример задается векторами.

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-комбинаторов, а 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)

Вы можете немного сэкономить, указав >>= более прямо, но каким бы способом вы его не отрезали, монадическое поведение создает бесполезные трюки для недиагональных вычислений. Лень может спасти нас от замедления с помощью армагеддона, но поведение застежки <*> должно быть по крайней мере немного дешевле, чем брать диагональ матрицы.

Ответ 3

Как сказал свиновод, массивы - очевидный пример; их экземпляр монады не только немного более проблематичен на концептуальном уровне с индексами, индексированными по типу и т.д., но и хуже работает в очень реалистичной реализации 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 нас (640001 итераций)
найдено 3768 выбросов среди 639999 образцов (0,6%)
2924 (0,5%) высокая степень тяжести
оценка стоимости часового звонка...
среднее значение - 41,62906 нс (12 итераций)
найдено 1 выброс среди 12 образцов (8,3%)
1 (8,3%) с высокой степенью тяжести

бенчмаркинг Monadic
среднее значение: 2,773062 мс, фунт 2,769786 мс, ub 2,779151 мс, ci 0,950
std dev: 22.14540 us, lb 13.55686 us, ub 36.88265 us, ci 0.950

сравнительный анализ среднее значение: 1.269351 мс, фунт 1.267654 мс, ub 1.271526 мс, ci 0,950
std dev: 9.799454 us, lb 8.171284 us, ub 13.09267 us, ci 0.950

Обратите внимание, что при компиляции с -O2 он не отличается разницей в производительности; очевидно, ap заменяется на <*>. Но >>= может выделять нужное количество памяти после каждого вызова функции, а затем помещать результаты на место, что, по-видимому, довольно дорогое время; тогда как <*> может просто прекомпотировать длину результата как произведение длин functions и values, а затем записать в один фиксированный массив.