Построение эффективных экземпляров монады в `Set` (и других контейнерах с ограничениями) с использованием продолжения монады
Set
, аналогично []
имеет совершенно определенные монадические операции. Проблема в том, что они требуют, чтобы значения удовлетворяли условию Ord
, и поэтому невозможно определить return
и >>=
без каких-либо ограничений. Эта же проблема применяется ко многим другим структурам данных, которые требуют каких-либо ограничений на возможные значения.
Стандартный трюк (предложенный мне в статье haskell-cafe) заключается в том, чтобы обернуть Set
в монаду продолжения. ContT
не имеет значения, имеет ли функтор базового типа какие-либо ограничения. Ограничения становятся необходимыми только при переносе/распаковке Set
в/из продолжений:
import Control.Monad.Cont
import Data.Foldable (foldrM)
import Data.Set
setReturn :: a -> Set a
setReturn = singleton
setBind :: (Ord b) => Set a -> (a -> Set b) -> Set b
setBind set f = foldl' (\s -> union s . f) empty set
type SetM r a = ContT r Set a
fromSet :: (Ord r) => Set a -> SetM r a
fromSet = ContT . setBind
toSet :: SetM r r -> Set r
toSet c = runContT c setReturn
Это работает по мере необходимости. Например, мы можем моделировать недетерминированную функцию, которая либо увеличивает свой аргумент на 1, либо оставляет его неповрежденным:
step :: (Ord r) => Int -> SetM r Int
step i = fromSet $ fromList [i, i + 1]
-- repeated application of step:
stepN :: Int -> Int -> Set Int
stepN times start = toSet $ foldrM ($) start (replicate times step)
Действительно, stepN 5 0
дает fromList [0,1,2,3,4,5]
. Если бы мы использовали вместо монады []
, мы получили бы
[0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5]
вместо.
Проблема эффективность. Если мы назовем stepN 20 0
, вывод займет несколько секунд, а stepN 30 0
не закончится в течение разумного промежутка времени. Оказывается, что все операции Set.union
выполняются в конце, а не выполняют их после каждого монадического вычисления. Результат состоит в том, что экспоненциально много Set
построены и union
ed только в конце, что неприемлемо для большинства задач.
Есть ли способ обойти это, чтобы сделать эту конструкцию эффективной? Я пробовал, но безуспешно.
(Я даже подозреваю, что могут существовать некоторые теоретические пределы, вытекающие из изоморфизма Карри-Говарда и теоремы Гливенко. Теорема Гливенко гласит, что для любой пропозициональной тавтологии φ формула ¬¬φ может быть доказана в интуиционистской логике. Однако я подозреваю, что длина доказательства (в нормальной форме) может быть экспоненциально длинной. Поэтому, возможно, могут быть случаи, когда перенос вычисления в монаду продолжения сделает ее экспоненциально длиннее?)
Ответы
Ответ 1
Монады - это один из способов структурирования и упорядочения вычислений. Связывание монады не может магически перестроить ваши вычисления, чтобы это произошло более эффективным образом. Есть две проблемы с тем, как вы структурируете свои вычисления.
-
При оценке stepN 20 0
результат step 0
будет вычисляться 20 раз. Это связано с тем, что каждый шаг вычисления дает 0 как одну альтернативу, которая затем подается на следующий шаг, который также дает 0 в качестве альтернативы и так далее...
Возможно, здесь может быть немного воспоминаний.
-
Гораздо большая проблема - это эффект ContT
на структуру вашего вычисления. С небольшим количеством эквациональных рассуждений, расширяя результат replicate 20 step
, определение foldrM
и упрощая столько раз, сколько необходимо, мы можем видеть, что stepN 20 0
эквивалентно:
(...(return 0 >>= step) >>= step) >>= step) >>= ...)
Все круглые скобки этого выражения связаны слева. Это замечательно, потому что это означает, что RHS каждого вхождения (>>=)
является элементарным вычислением, а именно step
, а не составленным. Однако, масштабируя определение (>>=)
для ContT
,
m >>= k = ContT $ \c -> runContT m (\a -> runContT (k a) c)
мы видим, что при оценке цепи (>>=)
, связанной с левым, каждое связывание подталкивает новое вычисление к текущему продолжению c
. Чтобы проиллюстрировать, что происходит, мы можем снова использовать немного эквациональных рассуждений, расширяя это определение для (>>=)
и определение для runContT
и упрощая, давая:
setReturn 0 `setBind`
(\x1 -> step x1 `setBind`
(\x2 -> step x2 `setBind` (\x3 -> ...)...)
Теперь, для каждого вхождения setBind
, спросите себя, что такое аргумент RHS. Для самого левого вхождения аргумент RHS - это весь остаток вычисления после setReturn 0
. Для второго вхождения это все после step x1
и т.д. Позвольте приблизиться к определению setBind
:
setBind set f = foldl' (\s -> union s . f) empty set
Здесь f
представляет все остальное вычисление, все в правой части вхождения setBind
. Это означает, что на каждом шаге мы собираем остальную часть вычисления как f
и применяем f
столько раз, сколько элементов в set
. Вычисления не являются элементарными, как раньше, а скорее составлены, и эти вычисления будут дублироваться много раз.
Суть проблемы заключается в том, что монадный трансформатор ContT
преобразует исходную структуру вычисления, которую вы имели в виду как левую ассоциативную цепочку setBind
, в вычисление с другой структурой, то есть правая ассоциативная цепь. Все это прекрасно, потому что один из законов монады говорит, что для каждого m
, f
и g
имеем
(m >>= f) >>= g = m >>= (\x -> f x >>= g)
Однако законы монады не налагают, что сложность остается неизменной с каждой стороны уравнений каждого закона. И действительно, в этом случае левый ассоциативный способ структурирования этого вычисления намного эффективнее. Левая ассоциативная цепочка setBind
мгновенно оценивается, потому что дублируются только элементарные подкомплексы.
Оказывается, что другие решения shoehorning set
в монаду также страдают от одной и той же проблемы. В частности, пакет set-monad дает аналогичные версии. Причина в том, что он тоже переписывает левые ассоциативные выражения в правильные ассоциативные.
Я думаю, вы указали на очень важную, но довольно тонкую проблему, настаивая на том, что set
подчиняется интерфейсу Monad
. И я не думаю, что это можно решить. Проблема в том, что тип привязки монады должен быть
(>>=) :: m a -> (a -> m b) -> m b
то есть ограничение класса не допускается ни на a
, ни на b
. Это означает, что мы не можем вложить привязки слева, не прибегая сначала к законам монады, чтобы переписать их в правую ассоциативную цепочку. Вот почему: при заданном (m >>= f) >>= g
тип вычисления (m >>= f)
имеет вид m b
. Значение вычисления (m >>= f)
имеет тип b
. Но поскольку мы не можем вложить ограничение класса в переменную типа b
, мы не можем знать, что полученное нами значение удовлетворяет ограничению Ord
и поэтому не может использовать это значение как элемент набора, на котором мы хотите иметь возможность вычислять union
.
Ответ 2
Недавно на Haskell Cafe Олег привел пример, как эффективно реализовать монаду Set
. Цитирование:
... И тем не менее, возможна эффективная подлинная монада Set.
... Прилагается эффективная подлинная монада. Я написал его в прямом стиле (кажется, быстрее, во всяком случае). Ключ состоит в том, чтобы использовать оптимизированную функцию выбора, когда мы можем.
{-# LANGUAGE GADTs, TypeSynonymInstances, FlexibleInstances #-}
module SetMonadOpt where
import qualified Data.Set as S
import Control.Monad
data SetMonad a where
SMOrd :: Ord a => S.Set a -> SetMonad a
SMAny :: [a] -> SetMonad a
instance Monad SetMonad where
return x = SMAny [x]
m >>= f = collect . map f $ toList m
toList :: SetMonad a -> [a]
toList (SMOrd x) = S.toList x
toList (SMAny x) = x
collect :: [SetMonad a] -> SetMonad a
collect [] = SMAny []
collect [x] = x
collect ((SMOrd x):t) = case collect t of
SMOrd y -> SMOrd (S.union x y)
SMAny y -> SMOrd (S.union x (S.fromList y))
collect ((SMAny x):t) = case collect t of
SMOrd y -> SMOrd (S.union y (S.fromList x))
SMAny y -> SMAny (x ++ y)
runSet :: Ord a => SetMonad a -> S.Set a
runSet (SMOrd x) = x
runSet (SMAny x) = S.fromList x
instance MonadPlus SetMonad where
mzero = SMAny []
mplus (SMAny x) (SMAny y) = SMAny (x ++ y)
mplus (SMAny x) (SMOrd y) = SMOrd (S.union y (S.fromList x))
mplus (SMOrd x) (SMAny y) = SMOrd (S.union x (S.fromList y))
mplus (SMOrd x) (SMOrd y) = SMOrd (S.union x y)
choose :: MonadPlus m => [a] -> m a
choose = msum . map return
test1 = runSet (do
n1 <- choose [1..5]
n2 <- choose [1..5]
let n = n1 + n2
guard $ n < 7
return n)
-- fromList [2,3,4,5,6]
-- Values to choose from might be higher-order or actions
test1' = runSet (do
n1 <- choose . map return $ [1..5]
n2 <- choose . map return $ [1..5]
n <- liftM2 (+) n1 n2
guard $ n < 7
return n)
-- fromList [2,3,4,5,6]
test2 = runSet (do
i <- choose [1..10]
j <- choose [1..10]
k <- choose [1..10]
guard $ i*i + j*j == k * k
return (i,j,k))
-- fromList [(3,4,5),(4,3,5),(6,8,10),(8,6,10)]
test3 = runSet (do
i <- choose [1..10]
j <- choose [1..10]
k <- choose [1..10]
guard $ i*i + j*j == k * k
return k)
-- fromList [5,10]
-- Test by Petr Pudlak
-- First, general, unoptimal case
step :: (MonadPlus m) => Int -> m Int
step i = choose [i, i + 1]
-- repeated application of step on 0:
stepN :: Int -> S.Set Int
stepN = runSet . f
where
f 0 = return 0
f n = f (n-1) >>= step
-- it works, but clearly exponential
{-
*SetMonad> stepN 14
fromList [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]
(0.09 secs, 31465384 bytes)
*SetMonad> stepN 15
fromList [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
(0.18 secs, 62421208 bytes)
*SetMonad> stepN 16
fromList [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
(0.35 secs, 124876704 bytes)
-}
-- And now the optimization
chooseOrd :: Ord a => [a] -> SetMonad a
chooseOrd x = SMOrd (S.fromList x)
stepOpt :: Int -> SetMonad Int
stepOpt i = chooseOrd [i, i + 1]
-- repeated application of step on 0:
stepNOpt :: Int -> S.Set Int
stepNOpt = runSet . f
where
f 0 = return 0
f n = f (n-1) >>= stepOpt
{-
stepNOpt 14
fromList [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14]
(0.00 secs, 515792 bytes)
stepNOpt 15
fromList [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
(0.00 secs, 515680 bytes)
stepNOpt 16
fromList [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
(0.00 secs, 515656 bytes)
stepNOpt 30
fromList [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
(0.00 secs, 1068856 bytes)
-}
Ответ 3
Я не думаю, что ваши проблемы с производительностью в этом случае связаны с использованием Cont
step' :: Int -> Set Int
step' i = fromList [i,i + 1]
foldrM' f z0 xs = Prelude.foldl f' setReturn xs z0
where f' k x z = f x z `setBind` k
stepN' :: Int -> Int -> Set Int
stepN' times start = foldrM' ($) start (replicate times step')
получает аналогичную производительность для реализации на основе Cont
, но полностью реализуется в Set
"ограниченной монаде"
Я не уверен, верю ли я ваше утверждение о теореме Гливенко, приводящее к экспоненциальному увеличению (нормализованного) размера доказательства - по крайней мере, в контексте Call-By-Need. Это потому, что мы можем произвольно использовать вторичные доказательства (и наша логика второго порядка, нам нужно только одно доказательство forall a. ~~(a \/ ~a)
). Доказательства - это не деревья, они - графики (разделение).
В общем, вы, вероятно, увидите затраты на производительность от Cont
wrapping Set
, но их обычно можно избежать с помощью
smash :: (Ord r, Ord k) => SetM r r -> SetM k r
smash = fromSet . toSet
Ответ 4
Я узнал еще одну возможность, основанную на расширении GRA ConstraintKinds. Идея состоит в том, чтобы переопределить Monad
так, чтобы она включала параметрическое ограничение допустимых значений:
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE RebindableSyntax #-}
import qualified Data.Foldable as F
import qualified Data.Set as S
import Prelude hiding (Monad(..), Functor(..))
class CFunctor m where
-- Each instance defines a constraint it valust must satisfy:
type Constraint m a
-- The default is no constraints.
type Constraint m a = ()
fmap :: (Constraint m a, Constraint m b) => (a -> b) -> (m a -> m b)
class CFunctor m => CMonad (m :: * -> *) where
return :: (Constraint m a) => a -> m a
(>>=) :: (Constraint m a, Constraint m b) => m a -> (a -> m b) -> m b
fail :: String -> m a
fail = error
-- [] instance
instance CFunctor [] where
fmap = map
instance CMonad [] where
return = (: [])
(>>=) = flip concatMap
-- Set instance
instance CFunctor S.Set where
-- Sets need Ord.
type Constraint S.Set a = Ord a
fmap = S.map
instance CMonad S.Set where
return = S.singleton
(>>=) = flip F.foldMap
-- Example:
-- prints fromList [3,4,5]
main = print $ do
x <- S.fromList [1,2]
y <- S.fromList [2,3]
return $ x + y
(Проблема с этим подходом заключается в том, что монадические значения являются функциями, такими как m (a -> b)
, потому что они не могут удовлетворять ограничениям, таким как Ord (a -> b)
. Поэтому нельзя использовать комбинаторы типа <*>
(или ap
) для этой ограниченной монады Set
.)