Monada pauzy

Monady potrafią robić wiele niesamowitych, szalonych rzeczy. Mogą tworzyć zmienne, które posiadają superpozycję wartości. Mogą one umożliwić dostęp do danych z przyszłości, zanim je obliczysz. Mogą one pozwolić na pisanie destrukcyjnych aktualizacji, ale nie do końca. A potem kontynuacja monady pozwala [[11]] łamać ludzkie umysły! zazwyczaj własne. ;-)

Ale oto wyzwanie: czy możesz zrobić monadę, która może być wstrzymana ?

data Pause s x
instance Monad (Pause s)
mutate :: (s -> s) -> Pause s ()
yield :: Pause s ()
step :: s -> Pause s () -> (s, Maybe (Pause s ()))

Monada jest rodzajem stan monad (stąd mutate, z oczywistą semantyką). Normalnie monada taka jak ta ma jakąś funkcję "run", która uruchamia obliczenia i przekazuje Ci stan końcowy. Ale Pause jest inna: dostarcza step funkcji, która uruchamia obliczenia, dopóki nie wywoła magicznej yield funkcji. Tutaj obliczenia są wstrzymywane, zwracając do rozmówcy wystarczającą ilość informacji, aby wznowić obliczenia później.

Dla dodatkowej awesomness: pozwól wywołującemu zmodyfikować stan pomiędzy step telefony. (Powyższe podpisy typów powinny na to pozwolić, na przykład.)


Przypadek użycia: często łatwo jest napisać kod, który robi coś złożonego, ale całkowity PITA przekształca go również do wyjścia stanów pośrednich w jego działaniu. Jeśli chcesz, aby użytkownik mógł zmienić coś w trakcie wykonywania, sprawy szybko się komplikują.

Pomysły na wdrożenie:

  • oczywiście można to zrobić za pomocą wątków, zamki i IO. Ale czy możemy stać się lepsi? ;-)

  • Coś szalonego z kontynuacją monady?

  • Może jakaś pisarska monada, gdzie yield zapisuje aktualny stan, a potem możemy "udawać" to step poprzez iterację stanów w dzienniku. (Oczywiście wyklucza to zmianę stanu między stopniami, ponieważ tak naprawdę nie" wstrzymujemy " niczego teraz.)

Author: Franky, 2012-04-20

6 answers

Jasne; po prostu pozwalasz dowolnemu obliczeniu albo zakończyć się wynikiem, albo zawiesić się, dając akcję do użycia przy wznowieniu, wraz ze stanem w tym czasie:

data Pause s a = Pause { runPause :: s -> (PauseResult s a, s) }

data PauseResult s a
    = Done a
    | Suspend (Pause s a)

instance Monad (Pause s) where
    return a = Pause (\s -> (Done a, s))
    m >>= k = Pause $ \s ->
        case runPause m s of
            (Done a, s') -> runPause (k a) s'
            (Suspend m', s') -> (Suspend (m' >>= k), s')

get :: Pause s s
get = Pause (\s -> (Done s, s))

put :: s -> Pause s ()
put s = Pause (\_ -> (Done (), s))

yield :: Pause s ()
yield = Pause (\s -> (Suspend (return ()), s))

step :: Pause s () -> s -> (Maybe (Pause s ()), s)
step m s =
    case runPause m s of
        (Done _, s') -> (Nothing, s')
        (Suspend m', s') -> (Just m', s')

Instancja Monad po prostu sekwencjonuje rzeczy w normalny sposób, przekazując końcowy wynik do k kontynuacji lub dodając resztę obliczeń do wykonania po zawieszeniu.

 53
Author: ehird,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-19 21:46:57

Uwaga: nie zapewniłeś sobie bezpośredniego dostępu do obecnego stanu s w tej monadzie.

Pause s jest tylko darmową monadą nad operacjami mutate i yield. Zaimplementowane bezpośrednio otrzymujesz:

data Pause s a
  = Return a
  | Mutate (s -> s) (Pause s a)
  | Yield (Pause s a)

instance Monad (Pause s) where
  return = Return
  Return a   >>= k = k a
  Mutate f p >>= k = Mutate f (p >>= k)
  Yield p    >>= k = Yield (p >>= k)

Z kilkoma inteligentnymi konstruktorami, aby dać ci pożądane API:

mutate :: (s -> s) -> Pause s ()
mutate f = Mutate f (return ())

yield :: Pause s ()
yield = Yield (return ())

I funkcja step, która go napędza

step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Mutate f k) = step (f s) k
step s (Return ()) = (s, Nothing)
step s (Yield k) = (s, Just k)

Możesz również zdefiniować to bezpośrednio za pomocą

data Free f a = Pure a | Free (f (Free f a))

(z mojego 'darmowego' pakietu) z

data Op s a = Mutate (s -> s) a | Yield a

Wtedy już mamy monada na pauzę

type Pause s = Free (Op s)

I wystarczy zdefiniować inteligentne konstruktory i stepper.

Szybciej.

Teraz, te implementacje są łatwe do rozumowania, ale nie mają najszybszego modelu operacyjnego. W szczególności lewe powiązane użycie ( > > = ) daje asymptotycznie wolniejszy kod.

Aby obejść to, możesz zastosować Codensity monad do istniejącej wolnej monad, lub po prostu użyć 'Church free' monad bezpośrednio, oba z nich opisuję szczegółowo na moim blogu.

Http://comonad.com/reader/2011/free-monads-for-less/

Http://comonad.com/reader/2011/free-monads-for-less-2/

Http://comonad.com/reader/2011/free-monads-for-less-3/

Rezultatem zastosowania kodowanej przez Kościół wersji darmowej monady jest to, że otrzymujesz łatwy do zrozumienia model dla typu danych, a nadal otrzymujesz szybki model oceny.

 60
Author: Edward KMETT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-19 21:57:18

Oto jak bym to zrobił, używając darmowych monad. Czym one są? Są to drzewa z akcjami na węzłach i wartościami na liściach, z >>= działającymi jak szczepienie drzew.

data f :^* x
  = Ret x
  | Do (f (f :^* x))

Nie jest niczym niezwykłym pisać F*X na takie coś w matematyce, stąd moja marudna nazwa typu infix. Aby utworzyć instancję, wystarczy f być czymś, co można odwzorować: zrobi to każda Functor.

instance Functor f => Monad ((:^*) f) where
  return = Ret
  Ret x  >>= k  = k x
  Do ffx >>= k  = Do (fmap (>>= k) ffx)

To po prostu "zastosuj k na wszystkie liście i przeszczep w powstałe drzewa". Drzewa mogą reprezentować strategie do obliczeń interaktywnych: całe drzewo obejmuje każdą możliwą interakcję z otoczeniem, a środowisko wybiera ścieżkę w drzewie, którą ma podążać. Dlaczego są one wolne ? To tylko drzewa, bez interesującej teorii równań, mówiące, które strategie są równoważne które inne strategie.

Teraz mamy zestaw do tworzenia funktorów, które odpowiadają kilku poleceniom we może chcesz być w stanie to zrobić. To coś

data (:>>:) s t x = s :? (t -> x)

instance Functor (s :>>: t) where
  fmap k (s :? f) = s :? (k . f)

Przechwytuje ideę uzyskania wartości w x po jednym poleceniu z typem wejścia s i typem wyjścia t. Aby to zrobić, musisz wybrać wejście w s i wyjaśnić, jak kontynuować do wartości w x, biorąc pod uwagę wyjście polecenia w t. Aby odwzorować funkcję na taką rzecz, przyczepiasz ją do kontynuacji. Do tej pory standardowe wyposażenie. Dla naszego problemu możemy teraz zdefiniować dwa funktory:

type Modify s  = (s -> s) :>>: ()
type Yield     = () :>>: ()

To tak jak właśnie spisałem typy wartości dla poleceń, które chcemy wykonać!

Teraz upewnijmy się, że możemy zaoferować wybór pomiędzy tymi poleceniami. Możemy pokazać, że wybór pomiędzy funktorami daje funktor. Więcej standardowego wyposażenia.

data (:+:) f g x = L (f x) | R (g x)

instance (Functor f, Functor g) => Functor (f :+: g) where
  fmap k (L fx) = L (fmap k fx)
  fmap k (R gx) = R (fmap k gx)

Zatem Modify s :+: Yield reprezentuje wybór pomiędzy modyfikacją a poddaniem. Każdy podpis prostych poleceń (komunikujących się ze światem pod względem wartości, a nie manipulujących obliczeniami) można obrócić do functora w ten sposób. To kłopot, że muszę to zrobić ręcznie!

To daje mi Twoją monadę: darmową monadę nad podpisem modyfikuj i plonuj.

type Pause s = (:^*) (Modify s :+: Yield)

Mogę zdefiniować polecenia modify i yield jako one-do-then-return. Poza negocjowaniem atrapy wejścia dla yield, to tylko mechaniczne.

mutate :: (s -> s) -> Pause s ()
mutate f = Do (L (f :? Ret))

yield :: Pause s ()
yield = Do (R (() :? Ret))

Funkcja step nadaje znaczenie drzewom strategii. Jest operatorem sterującym , konstruującym jeden (być może) z kolejny.

step :: s -> Pause s () -> (s, Maybe (Pause s ()))
step s (Ret ())            = (s, Nothing)
step s (Do (L (f  :? k)))  = step (f s) (k ())
step s (Do (R (() :? k)))  = (s, Just (k ()))

Funkcja step uruchamia strategię, dopóki nie zakończy się Ret, lub daje, mutując stan w miarę upływu czasu.

Ogólna metoda wygląda następująco: oddziel polecenia (interakcja pod względem wartości) od operatory sterowania (manipulowanie obliczeniami); zbuduj darmową monadę "drzew strategii" nad podpisem poleceń( uruchamianie uchwytu); zaimplementuj operatory sterowania przez rekurencję nad strategią drzewa.

 31
Author: pigworker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-19 22:15:53

Nie pasuje dokładnie do Twoich podpisów typu, ale na pewno proste:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, UndecidableInstances #-}
import Control.Monad.State

newtype ContinuableT m a = Continuable { runContinuable :: m (Either a (ContinuableT m a)) }
instance Monad m => Monad (ContinuableT m) where
    return = Continuable . return . Left
    Continuable m >>= f = Continuable $ do
        v <- m
        case v of
            Left  a -> runContinuable (f a)
            Right b -> return (Right (b >>= f))

instance MonadTrans ContinuableT where
    lift m = Continuable (liftM Left m)

instance MonadState s m => MonadState s (ContinuableT m) where
    get = lift get
    put = lift . put

yield :: Monad m => ContinuableT m a -> ContinuableT m a
yield = Continuable . return . Right

step :: ContinuableT (State s) a -> s -> (Either a (ContinuableT (State s) a), s)
step = runState . runContinuable

-- mutate unnecessary, just use modify
 11
Author: Daniel Wagner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-20 02:13:25
{-# LANGUAGE TupleSections #-}
newtype Pause s x = Pause (s -> (s, Either x (Pause s x)))

instance Monad (Pause s) where
  return x = Pause (, Left x)

  Pause k >>= f = Pause $ \s -> let (s', v) = k s in
                                case v of
                                  Left x -> step (f x) s'
                                  Right x -> (s', Right (x >>= f))

mutate :: (s -> s) -> Pause s ()
mutate f = Pause (\s -> (f s, Left ()))

yield :: Pause s ()
yield = Pause (, Right (return ()))

step :: Pause s x -> s -> (s, Either x (Pause s x))
step (Pause x) = x
Tak bym to napisał. Podałem step nieco bardziej ogólną definicję, równie dobrze mogłaby się nazywać runPause. W rzeczywistości myślenie o rodzaju step prowadzi mnie do definicji Pause.

W pakiecie monad-coroutine znajdziesz ogólny transformator monad. Monada Pause s jest taka sama jak Coroutine (State s) Id. Możesz łączyć koronki z innymi monadami.

Related: The Prompt monad in http://themonadreader.files.wordpress.com/2010/01/issue15.pdf

 8
Author: sdcvvc,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-19 22:18:19

Uwaga: Ta odpowiedź jest dostępna jako literat Haskell plik w Gist.

Bardzo podobało mi się to ćwiczenie. Próbowałem to zrobić bez patrzenia na odpowiedzi i było warto. Zajęło mi to sporo czasu, ale wynik jest zaskakująco Bliski dwóm innym odpowiedziom, a także bibliotece monad-coroutine . Więc myślę, że jest to nieco naturalne rozwiązanie tego problemu. Bez tego ćwiczenia, nie rozumiem, jak naprawdę monad-coroutine działa.

Aby dodać trochę dodatkowej wartości, wyjaśnię kroki, które ostatecznie doprowadziły mnie do rozwiązania.

Rozpoznawanie stanu monad

Ponieważ mamy do czynienia ze Stanami, szukamy wzorców, które mogą być skutecznie opisane przez monadę Stanów. W szczególności s - s jest izomorficzna z s -> (s, ()), więc może być zastąpiona przez State s (). I funkcja typu s -> x -> (s, y) może być zamieniona na x -> (s -> (s, y)), czyli w rzeczywistości x -> State s y. To prowadzi nas do aktualizacji podpisy

mutate :: State s () -    Pause s ()
step   :: Pause s () -    State s (Maybe (Pause s ()))

Uogólnienie

Nasz Pause monad jest obecnie parametryzowany przez państwo. Jednak teraz widzimy, że tak naprawdę do niczego nie potrzebujemy Państwa, ani nie używamy żadnych szczegółów monady Państwowej. Możemy więc spróbować stworzyć bardziej ogólne rozwiązanie, które jest parametryzowane przez dowolną monadę: {]}

mutate :: (Monad m) =    m () -> Pause m ()
yield  :: (Monad m) =    Pause m ()
step   :: (Monad m) =    Pause m () -> m (Maybe (Pause m ()))

Możemy również spróbować uczynić mutate i step bardziej ogólnymi, dopuszczając dowolny rodzaj wartości, a nie tylko (). I zdając sobie sprawę, że Maybe a jest izomorficzna do Możemy w końcu uogólnić nasze podpisy na]}

mutate :: (Monad m) =    m a -> Pause m a
yield  :: (Monad m) =    Pause m ()
step   :: (Monad m) =    Pause m a -> m (Either (Pause m a) a)

Tak, że step Zwraca wartość pośrednią obliczeń.

Transformator Monad

Teraz widzimy, że rzeczywiście staramy się zrobić monadę z monady-dodać jakąś dodatkową funkcjonalność. Jest to, co zwykle nazywa się transformatorem monad. Ponadto sygnatura mutate jest dokładnie taka sama jak lift Z MonadTrans. Najprawdopodobniej jesteśmy po prawej stronie. track.

Ostatnia monada

Funkcja step wydaje się być najważniejszą częścią naszej monady, definiuje dokładnie to, czego potrzebujemy. Być może, to może być nowa struktura danych? Spróbujmy:

import Control.Monad
import Control.Monad.Cont
import Control.Monad.State
import Control.Monad.Trans

data Pause m a
    = Pause { step :: m (Either (Pause m a) a) }

Jeśli Either jest Right, to jest to tylko wartość monadyczna, bez zawieszenia. To prowadzi nas do tego, jak wdrożyć easist thing - the lift funkcja z MonadTrans:

instance MonadTrans Pause where
    lift k = Pause (liftM Right k)

I mutate to po prostu specjalizacja:

mutate :: (Monad m) => m () -> Pause m ()
mutate = lift

Jeśli Either część to Left, reprezentuje ciągłość obliczeń po zawieszeniu. Więc stwórzmy dla tego funkcję:

suspend :: (Monad m) => Pause m a -> Pause m a
suspend = Pause . return . Left

Teraz yieldING obliczenie jest proste, po prostu zawieszamy z pustym obliczenie:

yield :: (Monad m) => Pause m ()
yield = suspend (return ())
/ Align = "left" / Instancja Monad. Naprawmy to. Implementacja return jest prosta, po prostu podnosimy wewnętrzną monadę. Implementacja >>= jest nieco trudniejsza. Jeśli oryginalna wartość {[18] } była tylko wartością prostą (Right y), to po prostu zawijamy f y jako wynik. Jeśli jest to wstrzymane obliczenie, które może być kontynuowane (Left p), to rekurencyjnie schodzimy do niego.
instance (Monad m) => Monad (Pause m) where
    return x = lift (return x) -- Pause (return (Right x))
    (Pause s) >>= f
        = Pause $ s >>= \x -> case x of
            Right y     -> step (f y)
            Left p      -> return (Left (p >>= f))

Testowanie

Spróbujmy zrobić jakąś modelową funkcję, która używa i aktualizuje stan, dając podczas gdy w obliczeniach:

test1 :: Int -> Pause (State Int) Int
test1 y = do
            x <- lift get
            lift $ put (x * 2)
            yield
            return (y + x)

I funkcja pomocnicza, która debuguje monadę-wypisuje jej kroki pośrednie do konsola:

debug :: Show s => s -> Pause (State s) a -> IO (s, a)
debug s p = case runState (step p) s of
                (Left next, s')     ->  print s' >> debug s' next
                (Right r, s')       ->  return (s', r)    

main :: IO ()
main = do
    debug 1000 (test1 1 >>= test1 >>= test1) >>= print

Wynik jest

2000
4000
8000
(8000,7001)

Zgodnie z oczekiwaniami.

Korutyny i monad-coroutine

Zaimplementowaliśmy dość ogólne rozwiązanie monadyczne, które implementuje Coroutines . Może nic dziwnego, że ktoś wpadł na ten pomysł wcześniej: -) i stworzył pakietmonad-coroutine . Mniej zaskakujące jest to, że jest bardzo podobne do tego, co stworzyliśmy.

Pakiet jeszcze bardziej uogólnia ideę. Kontynuacja obliczeń jest przechowywana wewnątrz dowolnego funktora. Pozwala to zawiesić wiele odmian jak do pracy z zawieszonymi obliczeniami. Na przykład, aby przekazać wartość do wywołującego wznowić (które wywołaliśmy step), lub poczekać na wartość aby kontynuować, itd.

 8
Author: Petr Pudlák,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-31 15:42:54