packages feed

sofetch-0.1.0.0: src/Fetch/Combinators.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}

module Fetch.Combinators
  ( -- * Fetch combinators
    fetchAll
  , fetchWith
  , fetchThrough
  , fetchMap
  , fetchMaybe
  , fetchMapWith
    -- * Lifted operators
    --
    -- | Operators for working with applicative values (e.g. fetched
    -- results) without explicit binding. Prefix @.@ distinguishes
    -- them from their pure counterparts.
    --
    -- @
    -- do userAge <- fetch (UserAge uid)
    --    pure (userAge >= 18)
    -- @
    --
    -- becomes:
    --
    -- @
    -- fetch (UserAge uid) .>= pure 18
    -- @
  , (.>)
  , (.<)
  , (.>=)
  , (.<=)
  , (.==)
  , (./=)
  , (.&&)
  , (.||)
  , (.++)
    -- * Applicative pairing
  , pair
    -- * Parallel short-circuiting
  , biselect
  , pAnd
  , pOr
    -- * Sequencing
  , andThen
    -- * Applicative filter
  , filterA
    -- * Error recovery
  , withDefault
  ) where

import Fetch.Class
import Fetch.Batched (Fetch(..))

import Control.Monad.Catch (MonadCatch, catch)
import Data.Foldable (toList)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM

-- ──────────────────────────────────────────────
-- Fetch combinators
-- ──────────────────────────────────────────────

-- | Fetch all keys, preserving the container shape.
--
-- @fetchAll [k1, k2, k3]@ batches all three keys into one round.
fetchAll :: (MonadFetch m n, DataSource m k, Typeable (Result k), Traversable t)
         => t k -> n (t (Result k))
fetchAll = traverse fetch

-- | Fetch all keys and pair each with its result.
fetchWith :: (MonadFetch m n, DataSource m k, Typeable (Result k), Traversable t)
          => t k -> n (t (k, Result k))
fetchWith = traverse (\k -> (,) k <$> fetch k)

-- | Extract a key from each element, fetch, and pair back.
--
-- @fetchThrough commentAuthor comments@ fetches all authors in one
-- round and pairs each comment with its author.
fetchThrough :: (MonadFetch m n, DataSource m k, Typeable (Result k), Traversable t)
             => (a -> k) -> t a -> n (t (a, Result k))
fetchThrough toKey = traverse (\a -> (,) a <$> fetch (toKey a))

-- | Extract a key from each element, fetch, and transform.
--
-- @fetchMap commentAuthor (\\c u -> CommentView c u) comments@
fetchMap :: (MonadFetch m n, DataSource m k, Typeable (Result k), Traversable t)
         => (a -> k) -> (a -> Result k -> b) -> t a -> n (t b)
fetchMap toKey combine = traverse (\a -> combine a <$> fetch (toKey a))

-- | Fetch a key if present.
fetchMaybe :: (MonadFetch m n, DataSource m k, Typeable (Result k))
           => Maybe k -> n (Maybe (Result k))
fetchMaybe = traverse fetch

-- | Fetch a collection of keys and return a map of results.
-- Duplicates are deduplicated.
fetchMapWith :: (MonadFetch m n, DataSource m k, Typeable (Result k), Foldable f)
             => f k -> n (HashMap k (Result k))
fetchMapWith ks =
  let keys = toList ks
  in HM.fromList <$> traverse (\k -> (,) k <$> fetch k) keys

-- ──────────────────────────────────────────────
-- Lifted operators
-- ──────────────────────────────────────────────

infixr 3 .&&
infixr 2 .||
infix  4 .>, .<, .>=, .<=, .==, ./=

-- | Lifted @(>)@.
(.>) :: (Ord a, Applicative f) => f a -> f a -> f Bool
(.>) = liftA2 (>)

-- | Lifted @(<)@.
(.<) :: (Ord a, Applicative f) => f a -> f a -> f Bool
(.<) = liftA2 (<)

-- | Lifted @(>=)@.
(.>=) :: (Ord a, Applicative f) => f a -> f a -> f Bool
(.>=) = liftA2 (>=)

-- | Lifted @(<=)@.
(.<=) :: (Ord a, Applicative f) => f a -> f a -> f Bool
(.<=) = liftA2 (<=)

-- | Lifted @(==)@.
(.==) :: (Eq a, Applicative f) => f a -> f a -> f Bool
(.==) = liftA2 (==)

-- | Lifted @(/=)@.
(./=) :: (Eq a, Applicative f) => f a -> f a -> f Bool
(./=) = liftA2 (/=)

-- | Short-circuiting lifted @(&&)@. Evaluates the second argument
-- only if the first returns 'True'.
(.&&) :: Monad m => m Bool -> m Bool -> m Bool
ma .&& mb = do a <- ma; if a then mb else pure False

-- | Short-circuiting lifted @(||)@. Evaluates the second argument
-- only if the first returns 'False'.
(.||) :: Monad m => m Bool -> m Bool -> m Bool
ma .|| mb = do a <- ma; if a then pure True else mb

-- | Lifted @(++)@.
(.++) :: Applicative f => f [a] -> f [a] -> f [a]
(.++) = liftA2 (++)

-- ──────────────────────────────────────────────
-- Applicative pairing
-- ──────────────────────────────────────────────

-- | Pair two applicative computations. When used with 'Fetch',
-- both sides are batched into the same round.
--
-- @pair (fetch userKey) (fetch postKey)@
pair :: Applicative f => f a -> f b -> f (a, b)
pair = liftA2 (,)

-- ──────────────────────────────────────────────
-- Parallel short-circuiting
-- ──────────────────────────────────────────────

infixr 5 `pAnd`
infixr 4 `pOr`

-- | Select from two computations that each return @Either a@.
--
-- Both sides are probed in the same round (their fetches are batched
-- together). If either side resolves to @Left a@, the other side is
-- abandoned and the result is @Left a@. Only when both sides resolve
-- to @Right@ are the two values paired.
--
-- After each round, resolved sides are checked for early exit so
-- that multi-round computations can short-circuit as soon as
-- possible.
--
-- This is the fundamental building block for 'pAnd' and 'pOr'.
biselect :: Monad m
         => Fetch m (Either a b)
         -> Fetch m (Either a c)
         -> Fetch m (Either a (b, c))
biselect = go
  where
    go l r = Fetch $ \e -> do
      sl <- unFetch l e
      sr <- unFetch r e
      pure $ case (sl, sr) of
        -- Either side short-circuits
        (Done (Left a), _)                -> Done (Left a)
        (_, Done (Left a))                -> Done (Left a)
        -- Both sides done
        (Done (Right b), Done (Right c))  -> Done (Right (b, c))
        -- One side done, the other blocked. Keep probing the
        -- blocked side each round in case it short-circuits.
        (Done (Right b), Blocked bs kr)   -> Blocked bs (goRight b kr)
        (Blocked bs kl, Done (Right c))   -> Blocked bs (goLeft kl c)
        -- Both blocked: merge batches, recurse next round
        (Blocked bs1 kl, Blocked bs2 kr)  -> Blocked (bs1 <> bs2) (go kl kr)

    -- Left resolved to @Right b@; keep probing right for early exit.
    goRight b r = Fetch $ \e -> do
      sr <- unFetch r e
      pure $ case sr of
        Done (Left a)    -> Done (Left a)
        Done (Right c)   -> Done (Right (b, c))
        Blocked bs kr    -> Blocked bs (goRight b kr)

    -- Right resolved to @Right c@; keep probing left for early exit.
    goLeft l c = Fetch $ \e -> do
      sl <- unFetch l e
      pure $ case sl of
        Done (Left a)    -> Done (Left a)
        Done (Right b)   -> Done (Right (b, c))
        Blocked bs kl    -> Blocked bs (goLeft kl c)

-- | Parallel @(&&)@. Both sides are probed in the same round
-- (batched together). If either side returns 'False' before the
-- other completes, the result is 'False' immediately; the other
-- side's remaining rounds are not evaluated.
--
-- Compare with '.&&' which is sequential short-circuiting
-- (left-to-right), and @liftA2 (&&)@ which is concurrent but
-- never short-circuits.
--
-- @
-- pAnd (fetch (IsActive uid)) (fetch (HasPermission uid "admin"))
-- @
pAnd :: Monad m => Fetch m Bool -> Fetch m Bool -> Fetch m Bool
pAnd x y = fromEither <$> biselect (discrim <$> x) (discrim <$> y)
  where
    discrim False = Left False  -- short-circuit
    discrim True  = Right ()    -- continue
    fromEither (Left b)  = b
    fromEither (Right _) = True

-- | Parallel @(||)@. Both sides are probed in the same round
-- (batched together). If either side returns 'True' before the
-- other completes, the result is 'True' immediately; the other
-- side's remaining rounds are not evaluated.
--
-- Compare with '.||' which is sequential short-circuiting
-- (left-to-right), and @liftA2 (||)@ which is concurrent but
-- never short-circuits.
--
-- @
-- pOr (fetch (IsAdmin uid)) (fetch (IsModerator uid))
-- @
pOr :: Monad m => Fetch m Bool -> Fetch m Bool -> Fetch m Bool
pOr x y = fromEither <$> biselect (discrim <$> x) (discrim <$> y)
  where
    discrim True  = Left True   -- short-circuit
    discrim False = Right ()    -- continue
    fromEither (Left b)  = b
    fromEither (Right _) = False

-- ──────────────────────────────────────────────
-- Sequencing
-- ──────────────────────────────────────────────

-- | Monadic sequencing: run the first computation, discard its
-- result, then run the second.
--
-- In a fetch monad where @('>>')@ equals @('*>')@ (i.e. both sides
-- are batched into one round), 'andThen' forces sequential execution
-- across round boundaries.
--
-- @
-- -- These batch together (one round):
-- fetch keyA *> fetch keyB
--
-- -- This forces two rounds:
-- fetch keyA \`andThen\` fetch keyB
-- @
andThen :: Monad m => m a -> m b -> m b
andThen a b = a >>= \_ -> b

-- ──────────────────────────────────────────────
-- Applicative filter
-- ──────────────────────────────────────────────

-- | Applicative version of 'Control.Monad.filterM'.
--
-- The predicate is applied to all elements via 'traverse', so when
-- used with a fetch monad, all predicate evaluations are batched
-- into the same round.
--
-- @
-- filterA (\\uid -> fetch (IsActive uid)) userIds
-- @
filterA :: Applicative f => (a -> f Bool) -> [a] -> f [a]
filterA predicate xs =
  filt <$> traverse predicate xs
  where
    filt bools = map fst $ filter snd $ zip xs bools

-- ──────────────────────────────────────────────
-- Error recovery
-- ──────────────────────────────────────────────

-- | Run a computation; if it throws any exception, return the
-- supplied default value instead.
--
-- @
-- userName <- withDefault "unknown" (fetch (UserName uid))
-- @
withDefault :: MonadCatch m => a -> m a -> m a
withDefault d a = a `catch` \(_ :: SomeException) -> pure d