Mam problem z projektowaniem w Haskell, którego nie mogę rozwiązać w sposób elegancki i satysfakcjonujący. Mam system, którego podstawą jest koncepcja pozyskiwania zdarzeń: stan systemu wynika z zastosowania sekwencji zdarzeń do stanu początkowego. Istnieją różne typy zdarzeń, każdy rodzaj jest związane z konkretnym składnikiem systemu przez rodziny typu:Jak napisać autobus na wydarzenie w Haskell?
class Model a where
data Event a :: *
apply :: Event a -> a -> a
instance Model Foo where
data Event Foo = Foo Int
...
instance Model Bar where
data Event Bar = Bar String
...
Obecnie system jest w 100% synchroniczne i sprzężony, każdy model mający dostęp do wydarzeń wszystko inny model i to szybko staje się bałagan, więc chcę, aby oddzielić rzeczy poprzez wprowadzenie magistrali zdarzeńBus Events
w taki sposób będę mógł napisać coś takiego dispatch :: Bus Events -> Consumer (Event Foo) -> Bus Events
dołączyć jakąś konsumenta Event Foo
do Bus Events
zakładając, że istnieje jakiś forma subpozycji lub subpozycji między Event Foo
i Events
. Następnie mogę dodać asynchroniczność, upewniając konsumentów, że każdy działa we własnych wątkach.
Z systemowego punktu widzenia pozwoliłoby to zapewnić, że każdy składnik jest pakowalny niezależnie, ograniczając zależności do podzbioru wszystkich zdarzeń. Typ Events
zostałby zdefiniowany na poziomie całej aplikacji. Ten problem wygląda na zwodniczo podobnego do dyskretnego FRP, ale nie potrafię się nim obejść ...
Czy ktoś już poradził sobie z czymś podobnym, a jeśli tak, to w jaki sposób?
EDIT:
wpadłem na następujący kod, który sprawia, że nie korzysta z Source
ale jest znacznie zainspirowany @ propozycję Cirdec za:
import Control.Applicative
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad.Reader
import qualified Data.Vector as V
type Handlers e = V.Vector (Handler e)
data EventBus e = EventBus { handlers :: Handlers e
, eventQueue :: TChan e
, eventThread :: MVar ThreadId
}
newBus :: IO (EventBus e)
newBus = do
chan <- newTChanIO
var <- newEmptyMVar
return $ EventBus V.empty chan var
addHandler :: Handler e -> EventBus e -> EventBus e
addHandler h [email protected]{..} = b { handlers = V.snoc handlers h }
removeHandler :: Int -> EventBus e -> EventBus e
removeHandler idx [email protected]{..} = b { handlers = let (h,t) = V.splitAt idx handlers
in h V.++ V.tail t }
startBus :: EventBus e -> IO (EventBus e)
startBus [email protected]{..} = do
tid <- forkIO (runBus b)
putMVar eventThread tid
return b
runBus :: EventBus e -> IO()
runBus [email protected]{..} = do
_ <- takeMVar eventThread
forever $ do
e <- liftIO $ atomically $ readTChan eventQueue
v <- newTVarIO b
runReaderT (runEvents $ publish e) v
-- | A monad to handle pub/sub of events of type @[email protected]
newtype Events e a = Events { runEvents :: ReaderT (TVar (EventBus e)) IO a }
deriving (Applicative, Functor, Monad, MonadIO, MonadReader (TVar (EventBus e)))
newtype Handler e = Handler { handle :: Events e() -- Unsubscription function
-> Events e (e -> Events e()) -- what to do with events @[email protected]
}
-- | Register a new @Handler [email protected] within given @Events [email protected] context
subscribe :: Handler e -> Events e()
subscribe h = do
bus <- ask
liftIO $ atomically $ modifyTVar' bus (addHandler h)
unsubscribe :: Int -> Events e()
unsubscribe idx = do
bus <- ask
liftIO $ atomically $ modifyTVar' bus (removeHandler idx)
publishBus :: EventBus e -> e -> IO()
publishBus EventBus{..} = atomically . writeTChan eventQueue
publish :: e -> Events e()
publish event = do
EventBus{..} <- ask >>= liftIO . atomically . readTVar
forM_ (zip (V.toList handlers) [0..]) (dispatch event)
dispatch :: e -> (Handler e, Int) -> Events e()
dispatch event (Handler h, idx) = do
hdl <- h (unsubscribe idx)
hdl event
printer :: (Show s) => String -> Handler s
printer prefix = Handler (\ _ -> return $ \ e -> liftIO (putStrLn $ prefix ++ show e))
więc szukasz czegoś podobnego do obserwowalnego wzorca dla swoich wydarzeń, prawda? IMO możesz go zaimplementować tak, jak zrobiłbyś to w OO (używając 'IORef' lub czegoś podobnego, jeśli chcesz zachować zachowanie typu" Subskrybuj "- lub daj obsługę kreatorom, jeśli nie chcesz) - pytanie brzmi: czy ty naprawdę? potrzebujesz tego, czy twoje polecenia/-handler * mogą być za to odpowiedzialne? – Carsten
@Carsten rodzaj, tak.Jeśli chodzi o sam system hydrauliczny, zamierzam użyć 'TChan', który ma fajną właściwość dostarczania funkcji pub/sub, ale to nie jest mój problem. Jestem bardziej zaniepokojony ogólnym podejściem do projektowania, wygląda na to, że próbuję zaimplementować coś w stylu wpisanych aktorów lub wymyślam FRP w dyskretnym czasie, ale jestem zdezorientowany. – insitu
Na czym polega problem, który próbujesz rozwiązać? Pytanie brzmi, jak rozwiązać problem w określony sposób, a nie jak rozwiązać problem. To całkiem możliwe, że rozwiązaniem jest coś, co już istnieje: wydarzenia w stylu oo, obserwowalne, FRP lub równoległe rury. – Cirdec