From 3faec852b0a7684c6d081cd15b41edd6b29e6585 Mon Sep 17 00:00:00 2001 From: Jens Nolte <git@queezle.net> Date: Wed, 21 Oct 2020 15:44:44 +0200 Subject: [PATCH] Refactor Observable to directly return value instead of Maybe There were some cases where the implicit Maybe was problematic, because it was impossible to guarantee a value. This also improved the `runQuery` return type (returning only an `Either` or an `Either e Maybe` is semantically easier to understand than the old `Maybe Either`. This introduced a lot of necessary changes, but the result is a better `runQuery` API: Every Query is now answered with either an exception detailing why the query failed, or with a result. --- src/lib/Qd/Observable.hs | 147 +++++++++++--------- src/lib/Qd/Observable/ObservableMap.hs | 8 +- src/lib/Qd/Observable/ObservablePriority.hs | 4 +- test/Qd/ObservableSpec.hs | 32 ++--- 4 files changed, 106 insertions(+), 85 deletions(-) diff --git a/src/lib/Qd/Observable.hs b/src/lib/Qd/Observable.hs index cf2901d..abd9a9f 100644 --- a/src/lib/Qd/Observable.hs +++ b/src/lib/Qd/Observable.hs @@ -13,12 +13,14 @@ module Qd.Observable ( MessageReason(..), ObservableVar, newObservableVar, - setObservableVar, modifyObservableVar, joinObservable, + joinObservableMaybe, + joinObservableMaybe', joinObservableEither, + joinObservableEither', mergeObservable, - mergeObservable', + mergeObservableMaybe, constObservable, FnObservable(..), ) where @@ -34,9 +36,9 @@ data MessageReason = Current | Update deriving (Eq, Show, Generic) instance Binary MessageReason -type ObservableMessage v = (MessageReason, Maybe v) +type ObservableMessage v = (MessageReason, v) -mapObservableMessage :: Monad m => (Maybe a -> m (Maybe b)) -> ObservableMessage a -> m (ObservableMessage b) +mapObservableMessage :: Monad m => (a -> m b) -> ObservableMessage a -> m (ObservableMessage b) mapObservableMessage f (r, s) = (r, ) <$> f s newtype SubscriptionHandle = SubscriptionHandle { unsubscribe :: IO () } @@ -52,24 +54,21 @@ instance Disposable a => Disposable (Maybe a) where dispose = mapM_ dispose class Observable v o | o -> v where - getValue :: o -> IO (Maybe v) + getValue :: o -> IO v subscribe :: o -> (ObservableMessage v -> IO ()) -> IO SubscriptionHandle - mapObservable :: (Maybe v -> Maybe a) -> o -> SomeObservable a + mapObservable :: (v -> a) -> o -> SomeObservable a mapObservable f = mapObservableM (return . f) - mapObservable' :: (v -> a) -> o -> SomeObservable a - mapObservable' f = mapObservable (fmap f) - mapObservableM :: (Maybe v -> IO (Maybe a)) -> o -> SomeObservable a + mapObservableM :: (v -> IO a) -> o -> SomeObservable a mapObservableM f = SomeObservable . MappedObservable f - mapObservableM' :: forall a. (v -> IO a) -> o -> SomeObservable a - mapObservableM' f = mapObservableM $ mapM f +-- | A variant of `subscribe` that passes the `SubscriptionHandle` to the callback. subscribe' :: Observable v o => o -> (SubscriptionHandle -> ObservableMessage v -> IO ()) -> IO SubscriptionHandle subscribe' observable callback = mfix $ \subscription -> subscribe observable (callback subscription) type ObservableCallback v = ObservableMessage v -> IO () instance Observable v o => Observable v (IO o) where - getValue :: IO o -> IO (Maybe v) + getValue :: IO o -> IO v getValue getObservable = getValue =<< getObservable subscribe :: IO o -> (ObservableMessage v -> IO ()) -> IO SubscriptionHandle subscribe getObservable callback = do @@ -88,20 +87,19 @@ instance Observable v (SomeObservable v) where subscribe (SomeObservable o) = subscribe o mapObservable f (SomeObservable o) = mapObservable f o mapObservableM f (SomeObservable o) = mapObservableM f o - mapObservableM' f (SomeObservable o) = mapObservableM' f o instance Functor SomeObservable where - fmap f = mapObservable' f + fmap f = mapObservable f -data MappedObservable b = forall a o. Observable a o => MappedObservable (Maybe a -> IO (Maybe b)) o +data MappedObservable b = forall a o. Observable a o => MappedObservable (a -> IO b) o instance Observable v (MappedObservable v) where getValue (MappedObservable f observable) = f =<< getValue observable subscribe (MappedObservable f observable) callback = subscribe observable (callback <=< mapObservableMessage f) mapObservableM f1 (MappedObservable f2 upstream) = SomeObservable $ MappedObservable (f1 <=< f2) upstream -newtype ObservableVar v = ObservableVar (MVar (Maybe v, HM.HashMap Unique (ObservableCallback v))) +newtype ObservableVar v = ObservableVar (MVar (v, HM.HashMap Unique (ObservableCallback v))) instance Observable v (ObservableVar v) where getValue (ObservableVar mvar) = fst <$> readMVar mvar subscribe (ObservableVar mvar) callback = do @@ -116,93 +114,114 @@ instance Observable v (ObservableVar v) where unsubscribe' key = modifyMVar_ mvar $ \(state, subscribers) -> return (state, HM.delete key subscribers) instance Settable v (ObservableVar v) where - setValue basicObservable = setObservableVar basicObservable . Just + setValue (ObservableVar mvar) value = modifyMVar_ mvar $ \(_, subscribers) -> do + mapM_ (\callback -> callback (Update, value)) subscribers + return (value, subscribers) -newObservableVar :: Maybe v -> IO (ObservableVar v) +newObservableVar :: v -> IO (ObservableVar v) newObservableVar initialValue = do ObservableVar <$> newMVar (initialValue, HM.empty) -setObservableVar :: ObservableVar v -> Maybe v -> IO () -setObservableVar (ObservableVar mvar) value = do - modifyMVar_ mvar $ \(_, subscribers) -> do - mapM_ (\callback -> callback (Update, value)) subscribers - return (value, subscribers) modifyObservableVar :: ObservableVar v -> (v -> v) -> IO () modifyObservableVar (ObservableVar mvar) f = modifyMVar_ mvar $ \(oldState, subscribers) -> do - let newState = (\v -> f v) <$> oldState + let newState = f oldState mapM_ (\callback -> callback (Update, newState)) subscribers return (newState, subscribers) + newtype JoinedObservable o = JoinedObservable o instance forall o i v. (Observable i o, Observable v i) => Observable v (JoinedObservable o) where - getValue :: JoinedObservable o -> IO (Maybe v) - getValue (JoinedObservable outer) = do + getValue :: JoinedObservable o -> IO v + getValue (JoinedObservable outer) = getValue =<< getValue outer + subscribe :: (JoinedObservable o) -> (ObservableMessage v -> IO ()) -> IO SubscriptionHandle + subscribe (JoinedObservable outer) callback = do + innerSubscriptionMVar <- newMVar dummySubscription + outerSubscription <- subscribe outer (outerCallback innerSubscriptionMVar) + return $ SubscriptionHandle{unsubscribe = unsubscribe outerSubscription} + where + dummySubscription = SubscriptionHandle { unsubscribe = return () } + outerCallback innerSubscriptionMVar = outerSubscription' + where + outerSubscription' (_, inner) = do + unsubscribe =<< takeMVar innerSubscriptionMVar + innerSubscription <- subscribe inner callback + putMVar innerSubscriptionMVar innerSubscription + +joinObservable :: (Observable i o, Observable v i) => o -> SomeObservable v +joinObservable = SomeObservable . JoinedObservable + + +newtype JoinedObservableMaybe o = JoinedObservableMaybe o +instance forall o i v. (Observable (Maybe i) o, Observable v i) => Observable (Maybe v) (JoinedObservableMaybe o) where + getValue :: JoinedObservableMaybe o -> IO (Maybe v) + getValue (JoinedObservableMaybe outer) = do state <- getValue outer case state of - Just inner -> getValue inner + Just inner -> Just <$> getValue inner Nothing -> return Nothing - subscribe :: (JoinedObservable o) -> (ObservableMessage v -> IO ()) -> IO SubscriptionHandle - subscribe (JoinedObservable outer) handler = do + subscribe :: (JoinedObservableMaybe o) -> (ObservableMessage (Maybe v) -> IO ()) -> IO SubscriptionHandle + subscribe (JoinedObservableMaybe outer) callback = do innerSubscriptionMVar <- newMVar dummySubscription outerSubscription <- subscribe outer (outerHandler innerSubscriptionMVar) return $ SubscriptionHandle{unsubscribe = unsubscribe outerSubscription} where dummySubscription = SubscriptionHandle { unsubscribe = return () } - outerHandler innerSubscriptionMVar = outerHandler' + outerHandler innerSubscriptionMVar = outerSubscription' where - outerHandler' (_, Just inner) = do + outerSubscription' (_, Just inner) = do unsubscribe =<< takeMVar innerSubscriptionMVar - innerSubscription <- subscribe inner handler + innerSubscription <- subscribe inner (callback . fmap Just) putMVar innerSubscriptionMVar innerSubscription - outerHandler' (reason, Nothing) = do + outerSubscription' (reason, Nothing) = do unsubscribe =<< takeMVar innerSubscriptionMVar - handler (reason, Nothing) + callback (reason, Nothing) putMVar innerSubscriptionMVar dummySubscription -joinObservable :: (Observable i o, Observable v i) => o -> SomeObservable v -joinObservable = SomeObservable . JoinedObservable +joinObservableMaybe :: forall o i v. (Observable (Maybe i) o, Observable v i) => o -> SomeObservable (Maybe v) +joinObservableMaybe = SomeObservable . JoinedObservableMaybe + +joinObservableMaybe' :: (Observable (Maybe i) o, Observable (Maybe v) i) => o -> SomeObservable (Maybe v) +joinObservableMaybe' = fmap join . joinObservableMaybe newtype JoinedObservableEither o = JoinedObservableEither o instance forall e o i v. (Observable (Either e i) o, Observable v i) => Observable (Either e v) (JoinedObservableEither o) where - getValue :: JoinedObservableEither o -> IO (Maybe (Either e v)) + getValue :: JoinedObservableEither o -> IO (Either e v) getValue (JoinedObservableEither outer) = do state <- getValue outer case state of - Just (Right inner) -> Right <$$> getValue inner - Just (Left ex) -> return $ Just $ Left ex - Nothing -> return Nothing + Right inner -> Right <$> getValue inner + Left ex -> return $ Left ex subscribe :: (JoinedObservableEither o) -> (ObservableMessage (Either e v) -> IO ()) -> IO SubscriptionHandle - subscribe (JoinedObservableEither outer) handler = do + subscribe (JoinedObservableEither outer) callback = do innerSubscriptionMVar <- newMVar dummySubscription outerSubscription <- subscribe outer (outerHandler innerSubscriptionMVar) return $ SubscriptionHandle{unsubscribe = unsubscribe outerSubscription} where dummySubscription = SubscriptionHandle { unsubscribe = return () } - outerHandler innerSubscriptionMVar = outerHandler' + outerHandler innerSubscriptionMVar = outerSubscription' where - outerHandler' (_, Just (Right inner)) = do + outerSubscription' (_, Right inner) = do unsubscribe =<< takeMVar innerSubscriptionMVar - innerSubscription <- subscribe inner (handler . fmap (fmap Right)) + innerSubscription <- subscribe inner (callback . fmap Right) putMVar innerSubscriptionMVar innerSubscription - outerHandler' (reason, Just (Left ex)) = do + outerSubscription' (reason, Left ex) = do unsubscribe =<< takeMVar innerSubscriptionMVar - handler (reason, Just (Left ex)) - putMVar innerSubscriptionMVar dummySubscription - outerHandler' (reason, Nothing) = do - unsubscribe =<< takeMVar innerSubscriptionMVar - handler (reason, Nothing) + callback (reason, Left ex) putMVar innerSubscriptionMVar dummySubscription joinObservableEither :: (Observable (Either e i) o, Observable v i) => o -> SomeObservable (Either e v) joinObservableEither = SomeObservable . JoinedObservableEither +joinObservableEither' :: (Observable (Either e i) o, Observable (Either e v) i) => o -> SomeObservable (Either e v) +joinObservableEither' = mapObservable join . JoinedObservableEither + -data MergedObservable o0 v0 o1 v1 r = MergedObservable (Maybe v0 -> Maybe v1 -> Maybe r) o0 o1 +data MergedObservable o0 v0 o1 v1 r = MergedObservable (v0 -> v1 -> r) o0 o1 instance forall o0 v0 o1 v1 r. (Observable v0 o0, Observable v1 o1) => Observable r (MergedObservable o0 v0 o1 v1 r) where getValue (MergedObservable merge obs0 obs1) = do x0 <- getValue obs0 @@ -214,13 +233,15 @@ instance forall o0 v0 o1 v1 r. (Observable v0 o0, Observable v1 o1) => Observabl sub1 <- subscribe obs1 (mergeCallback currentValuesTupleRef . fmap Right) return $ SubscriptionHandle{unsubscribe = unsubscribe sub0 >> unsubscribe sub1} where - mergeCallback :: IORef (Maybe v0, Maybe v1) -> (MessageReason, Either (Maybe v0) (Maybe v1)) -> IO () + mergeCallback :: IORef (Maybe v0, Maybe v1) -> (MessageReason, Either v0 v1) -> IO () mergeCallback currentValuesTupleRef (reason, state) = do currentTuple <- atomicModifyIORef' currentValuesTupleRef (dup . updateTuple state) - callback (reason, uncurry merge $ currentTuple) - updateTuple :: Either (Maybe v0) (Maybe v1) -> (Maybe v0, Maybe v1) -> (Maybe v0, Maybe v1) - updateTuple (Left l) (_, r) = (l, r) - updateTuple (Right r) (l, _) = (l, r) + case currentTuple of + (Just l, Just r) -> callback (reason, uncurry merge (l, r)) + _ -> return () -- Start only once both values have been received + updateTuple :: Either v0 v1 -> (Maybe v0, Maybe v1) -> (Maybe v0, Maybe v1) + updateTuple (Left l) (_, r) = (Just l, r) + updateTuple (Right r) (l, _) = (l, Just r) dup :: a -> (a, a) dup x = (x, x) @@ -228,17 +249,17 @@ instance forall o0 v0 o1 v1 r. (Observable v0 o0, Observable v1 o1) => Observabl -- | Merge two observables using a given merge function. Whenever the value of one of the inputs changes, the resulting observable updates according to the merge function. -- -- There is no caching involed, every subscriber effectively subscribes to both input observables. -mergeObservable :: (Observable v0 o0, Observable v1 o1) => (Maybe v0 -> Maybe v1 -> Maybe r) -> o0 -> o1 -> SomeObservable r +mergeObservable :: (Observable v0 o0, Observable v1 o1) => (v0 -> v1 -> r) -> o0 -> o1 -> SomeObservable r mergeObservable merge x y = SomeObservable $ MergedObservable merge x y --- | Like `mergeObservable`, but with a simplified signature that ignores the Maybe wrapper: If either value is `Nothing`, the resulting value will be `Nothing`. -mergeObservable' :: (Observable v0 o0, Observable v1 o1) => (v0 -> v1 -> r) -> o0 -> o1 -> SomeObservable r -mergeObservable' merge x y = SomeObservable $ MergedObservable (liftA2 merge) x y +-- | Similar to `mergeObservable`, but built to operator on `Maybe` values: If either input value is `Nothing`, the resulting value will be `Nothing`. +mergeObservableMaybe :: (Observable (Maybe v0) o0, Observable (Maybe v1) o1) => (v0 -> v1 -> r) -> o0 -> o1 -> SomeObservable (Maybe r) +mergeObservableMaybe merge x y = SomeObservable $ MergedObservable (liftA2 merge) x y -- | Data type that can be used as an implementation for the `Observable` interface that works by directly providing functions for `getValue` and `subscribe`. data FnObservable v = FnObservable { - getValueFn :: IO (Maybe v), + getValueFn :: IO v, subscribeFn :: (ObservableMessage v -> IO ()) -> IO SubscriptionHandle } instance Observable v (FnObservable v) where @@ -250,14 +271,14 @@ instance Observable v (FnObservable v) where } -newtype ConstObservable a = ConstObservable (Maybe a) +newtype ConstObservable a = ConstObservable a instance Observable a (ConstObservable a) where getValue (ConstObservable x) = return x subscribe (ConstObservable x) callback = do callback (Current, x) return $ SubscriptionHandle { unsubscribe = return () } -- | Create an observable that contains a constant value. -constObservable :: Maybe a -> SomeObservable a +constObservable :: a -> SomeObservable a constObservable = SomeObservable . ConstObservable diff --git a/src/lib/Qd/Observable/ObservableMap.hs b/src/lib/Qd/Observable/ObservableMap.hs index 418a882..82451dc 100644 --- a/src/lib/Qd/Observable/ObservableMap.hs +++ b/src/lib/Qd/Observable/ObservableMap.hs @@ -21,7 +21,7 @@ newtype ObservableMap k v = ObservableMap (MVar (HM.HashMap k (ObservableValue v data ObservableValue v = ObservableValue { value :: Maybe v, - subscribers :: (HM.HashMap Unique (ObservableMessage v -> IO ())) + subscribers :: (HM.HashMap Unique (ObservableMessage (Maybe v) -> IO ())) } modifyValue :: forall k v a. (Eq k, Hashable k) => (ObservableValue v -> IO (ObservableValue v, a)) -> k -> ObservableMap k v -> IO a @@ -38,18 +38,18 @@ modifyValue f k (ObservableMap mvar) = modifyMVar mvar $ \hashmap -> runGetT (HM modifyValue_ :: forall k v. (Eq k, Hashable k) => (ObservableValue v -> IO (ObservableValue v)) -> k -> ObservableMap k v -> IO () modifyValue_ f = modifyValue (fmap (,()) . f) -modifySubscribers :: (HM.HashMap Unique (ObservableMessage v -> IO ()) -> HM.HashMap Unique (ObservableMessage v -> IO ())) -> ObservableValue v -> ObservableValue v +modifySubscribers :: (HM.HashMap Unique (ObservableMessage (Maybe v) -> IO ()) -> HM.HashMap Unique (ObservableMessage (Maybe v) -> IO ())) -> ObservableValue v -> ObservableValue v modifySubscribers f ov@ObservableValue{subscribers} = ov{subscribers=f subscribers} create :: IO (ObservableMap k v) create = ObservableMap <$> newMVar HM.empty -observeKey :: forall k v. (Eq k, Hashable k) => k -> ObservableMap k v -> SomeObservable v +observeKey :: forall k v. (Eq k, Hashable k) => k -> ObservableMap k v -> SomeObservable (Maybe v) observeKey key om@(ObservableMap mvar) = SomeObservable FnObservable{getValueFn, subscribeFn} where getValueFn :: IO (Maybe v) getValueFn = (value <=< HM.lookup key) <$> readMVar mvar - subscribeFn :: ((ObservableMessage v -> IO ()) -> IO SubscriptionHandle) + subscribeFn :: ((ObservableMessage (Maybe v) -> IO ()) -> IO SubscriptionHandle) subscribeFn callback = do subscriptionKey <- newUnique modifyValue_ (subscribeFn' subscriptionKey) key om diff --git a/src/lib/Qd/Observable/ObservablePriority.hs b/src/lib/Qd/Observable/ObservablePriority.hs index 9f180fe..94cd8ef 100644 --- a/src/lib/Qd/Observable/ObservablePriority.hs +++ b/src/lib/Qd/Observable/ObservablePriority.hs @@ -19,7 +19,7 @@ type Entry v = (Unique, v) -- | Mutable data structure that stores values of type "v" with an assiciated priority "p". The `Observable` instance can be used to get or observe the value with the highest priority. data ObservablePriority p v = ObservablePriority (MVar (Internals p v)) -instance Observable v (ObservablePriority p v) where +instance Observable (Maybe v) (ObservablePriority p v) where getValue (ObservablePriority mvar) = getValueFromInternals <$> readMVar mvar where getValueFromInternals :: Internals p v -> Maybe v @@ -41,7 +41,7 @@ type PriorityMap p v = HM.HashMap p (NonEmpty (Entry v)) data Internals p v = Internals { priorityMap :: PriorityMap p v, current :: Maybe (Unique, p, v), - subscribers :: HM.HashMap Unique (ObservableCallback v) + subscribers :: HM.HashMap Unique (ObservableCallback (Maybe v)) } -- | Create a new `ObservablePriority` data structure. diff --git a/test/Qd/ObservableSpec.hs b/test/Qd/ObservableSpec.hs index b5417d9..8268fa8 100644 --- a/test/Qd/ObservableSpec.hs +++ b/test/Qd/ObservableSpec.hs @@ -15,41 +15,41 @@ mergeObservableSpec :: Spec mergeObservableSpec = do describe "mergeObservable" $ parallel $ do it "merges correctly using getValue" $ do - a <- newObservableVar Nothing - b <- newObservableVar Nothing + a <- newObservableVar "" + b <- newObservableVar "" - let mergedObservable = mergeObservable (\v0 v1 -> Just (v0, v1)) a b - let latestShouldBe = (getValue mergedObservable `shouldReturn`) . Just + let mergedObservable = mergeObservable (\v0 v1 -> (v0, v1)) a b + let latestShouldBe = (getValue mergedObservable `shouldReturn`) testSequence a b latestShouldBe it "merges correctly using subscribe" $ do - a <- newObservableVar Nothing - b <- newObservableVar Nothing + a <- newObservableVar "" + b <- newObservableVar "" - let mergedObservable = mergeObservable (\v0 v1 -> Just (v0, v1)) a b - (latestRef :: IORef (Maybe (Maybe String, Maybe String))) <- newIORef Nothing + let mergedObservable = mergeObservable (\v0 v1 -> (v0, v1)) a b + (latestRef :: IORef (String, String)) <- newIORef ("", "") void $ subscribe mergedObservable (writeIORef latestRef . snd) - let latestShouldBe = ((readIORef latestRef) `shouldReturn`) . Just + let latestShouldBe = ((readIORef latestRef) `shouldReturn`) testSequence a b latestShouldBe where - testSequence :: ObservableVar String -> ObservableVar String -> ((Maybe String, Maybe String) -> IO ()) -> IO () + testSequence :: ObservableVar String -> ObservableVar String -> ((String, String) -> IO ()) -> IO () testSequence a b latestShouldBe = do - latestShouldBe (Nothing, Nothing) + latestShouldBe ("", "") setValue a "a0" - latestShouldBe (Just "a0", Nothing) + latestShouldBe ("a0", "") setValue b "b0" - latestShouldBe (Just "a0", Just "b0") + latestShouldBe ("a0", "b0") setValue a "a1" - latestShouldBe (Just "a1", Just "b0") + latestShouldBe ("a1", "b0") setValue b "b1" - latestShouldBe (Just "a1", Just "b1") + latestShouldBe ("a1", "b1") -- No change setValue a "a1" - latestShouldBe (Just "a1", Just "b1") + latestShouldBe ("a1", "b1") -- GitLab