packages feed

batching-0.1.0.0: src/Control/Batching.hs

-- Copyright 2019-2021 Google LLC
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
--      http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

-- | An 'Applicative' for deferring "requests" to handle them all in bulk.

module Control.Batching
         ( Batching, request, batchRequest, runBatching, runBatching_
         ) where

import Control.Applicative (Applicative(..))
import Control.Monad.ST (runST)
import Data.Foldable (sequenceA_, toList)
import qualified Data.Foldable as F
import Data.Functor.Identity (Identity(..))
import GHC.TypeNats (type (+), Nat)

import qualified Data.Primitive.Array as A
import Data.SInt (SInt(unSInt), reifySInt, withSInt, sintVal, addSInt)
import Data.Vec.Short (Vec)
import qualified Data.Vec.Short as Vec

-- Quick-and-dirty Vec builder standin: a list.
--
-- Supports O(1) cons, O(n) conversion to Vec, and O(m) prepend of length-m to
-- length-n.
--
-- Note we won't ever left-associate appends, because we design the 'Batching'
-- type to avoid it, so O(m) prepend won't lead to any super-linear behavior.
newtype VecBuilder (n :: Nat) a = VecBuilder
  { _vbContents :: [a]
  }

nil :: VecBuilder 0 a
nil = VecBuilder []

cons :: a -> VecBuilder n a -> VecBuilder (1+n) a
cons x (VecBuilder xs) = VecBuilder (x:xs)

vbToVec' :: SInt n -> VecBuilder n a -> Vec n a
vbToVec' n (VecBuilder c) = reifySInt n $ Vec.fromList c

-- Quick-and-dirty Vec iterator standin: a list.
--
-- Supports O(1) uncons, O(m) bulk-uncons (split) of length m, and O(n)
-- conversion from Vec.
--
-- Note we won't ever recursively split, since splits are only generated by
-- 'batchRequest', and not by ('<*>'); so O(m) split won't lead to any
-- super-linear behavior.
newtype VecView (n :: Nat) a = VecView { _unVecView :: [a] }

uncons :: VecView (1+n) a -> (a, VecView n a)
uncons (VecView (x:xs)) = (x, VecView xs)
uncons _ = error "Internal error: invalid VecView."

split
  :: forall m n a. SInt m -> VecView (m+n) a -> (VecView m a, VecView n a)
split m (VecView xs) =
  let (ma, na) = splitAt (unSInt m) xs
  in  (VecView ma, VecView na)

vvFromVec :: Vec n a -> VecView n a
vvFromVec = VecView . toList

-- TODO(awpr): consider factoring out a @SizedBatch n rq rs a@ type and
-- providing a scoped API for accessing it, e.g.:
--
-- withSizedBatch
--   :: Batching rq rs a
--   -> (forall n. KnownNat n => SizedBatch n rq rs a -> r)
--   -> r
-- getBatchRequests  :: SizedBatch n rq rs a -> Vec n rq
-- putBatchResponses :: SizedBatch n rq rs a -> Vec n rs -> a

-- -- Start with a simple version: contains the requests and a function to --
-- consume the responses.
-- data Batching0 rq rs a = forall n. Batching0
--   { n        :: SInt n
--   , requests :: VecBuilder n rq
--   , cont     :: VecView n rs -> a
--   }
--
-- -- First transformation: actually use the curried (<**>) type internally so
-- -- that we can right-associate all of the request prepends.
-- newtype Batching1 rq rs a = Batching1
--   { _unBatching1 :: forall r. Batching0 rq rs (a -> r) -> Batching0 rq rs r
--   }
--
-- -- Second transformation: turn the above function inside out, to enable the
-- -- third transformation.
-- newtype Batching2 rq rs a = Batching2
--   { _unBatching2
--       :: forall z r. (Batching0 rq rs r -> z) -> (Batching0 rq rs (a -> r) -> z)
--   }
--
-- -- Third transformation: curry both of the function types above to get rid
-- -- of a GADT constructor.
-- newtype Batching rq rs a = Batching
--   { _unBatching
--       :: forall z r
--        . (forall n. SInt n -> (VecView n rs -> r)      -> VecBuilder n rq -> z)
--       -> (forall m. SInt m -> (VecView m rs -> a -> r) -> VecBuilder m rq -> z)
--   }
--
-- Now we have a newtype Batching that:
--   - takes a continuation for what to do with a VecBuilder of requests and a
--   VecView-consuming function returning @r@, with matching lengths.
--   - takes a VecView-consuming function for a (smaller) VecView returning
--   @a -> r@.
--   - takes a (smaller) VecBuilder of requests.
--   - wraps the VecView-consuming function with code to consume a prefix and
--   apply away the @a@ parameter.
--   - prepends some of its own requests to the VecBuilder.
--   - passes them on to the continuation and returns its result.

-- | The bulk request-response Applicative.
--
-- A value of type @Batching rq rs a@ describes a computation that gathers some
-- number of @rq@ request values, expects the same number of @rs@ response
-- values, and ultimately returns an @a@ result value derived from the
-- responses.
--
-- This can be used to apply an offline resource allocation algorithm to code
-- written as if allocation requests were satisfied incrementally.
--
-- This synergizes well with @-XApplicativeDo@, which allows using do-notation
-- for this type, as long as requests do not depend on earlier responses.
newtype Batching rq rs a = Batching
-- This is essentially the same as the Ap type from
-- Control.Applicative.Free.Fast, but specialized to lists of requests and
-- responses.
  { _unBulk
      :: forall z r
       . (forall n. SInt n -> (VecView n rs -> r)      -> VecBuilder n rq -> z)
      -> (forall m. SInt m -> (VecView m rs -> a -> r) -> VecBuilder m rq -> z)
  }

instance Functor (Batching rq rs) where
  fmap f (Batching go) = Batching $ \k m g rqs -> go k m (\vv x -> g vv (f x)) rqs
  {-# INLINE fmap #-}

instance Applicative (Batching rq rs) where
  pure x = Batching $ \k m g rqs -> k m (`g` x) rqs
  {-# INLINE pure #-}
  Batching f <*> Batching x =
    Batching $ \k m g rqs -> f (x k) m (\vv h -> g vv . h) rqs
  {-# INLINE (<*>) #-}
  liftA2 f (Batching x) (Batching y) = Batching $ \k m g rqs ->
    x (y k) m (\vv a b -> g vv (f a b)) rqs
  {-# INLINE liftA2 #-}

-- | Issue one request and retrieve its response.
request :: rq -> Batching rq rs rs
request rq = Batching $ \k n g !rqs -> k
  (sintVal `addSInt` n)
  -- Consume our response off the front of the VecView and pass it to the
  -- result function.
  (\rss -> let !(rs, rss') = uncons rss in g rss' rs)
  -- Add our request to the front of the VecBuilder.
  (rq `cons` rqs)
{-# INLINE request #-}

-- Specialized implementation for issuing Traversables of requests at once.
-- The goal here is to separate the traversal gathering requests from the
-- traversal building up the result, so we don't have to allocate a bunch of
-- memory to hold a closure that will build the resulting data structure while
-- we're handling the requests.
newtype BulkCont rq rs a = BulkCont
  { _bcCont :: A.Array rs -> Int -> (Int, a)
  }

instance Functor (BulkCont rq rs) where
  fmap f (BulkCont k) = BulkCont (\a i ->
    let !(i', x) = k a i
    in  (i', f x))
  {-# INLINE fmap #-}

instance Applicative (BulkCont rq rs) where
  pure x = BulkCont (\_ i -> (i, x))
  {-# INLINE pure #-}

  BulkCont kf <*> BulkCont kx = BulkCont (\a i ->
    let !(i', f) = kf a i
        !(i'', x) = kx a i'
    in  (i'', f x))
  {-# INLINE (<*>) #-}

  liftA2 f (BulkCont kx) (BulkCont ky) = BulkCont (\a i ->
    let !(i', x) = kx a i
        !(i'', y) = ky a i'
    in  (i'', f x y))
  {-# INLINE liftA2 #-}

rqBulkCont :: rq -> BulkCont rq rs rs
rqBulkCont _ = BulkCont $ \a i ->
  case A.indexArrayM a i of ((), x) -> (1 + i, x)
{-# INLINE rqBulkCont #-}

vvToArray :: forall n a. SInt n -> VecView n a -> A.Array a
vvToArray n (VecView v) = runST $
  A.newArray (unSInt n) undefined >>= \arr -> do
    sequenceA_ $ zipWith (A.writeArray arr) [0..] v
    A.unsafeFreezeArray arr

-- | Issue a Traversable of requests and retrieve their responses.
batchRequest :: forall t rq rs. Traversable t => t rq -> Batching rq rs (t rs)
batchRequest rqs0 =
  let !rqs = \rest -> F.foldr (:) rest rqs0
      !n = length rqs0
  in  withSInt n $ \sn ->
        Batching $ \k m g (VecBuilder rqs1) -> k
          (sn `addSInt` m)
          (\rss ->
            let !(rss0, rss1) = split sn rss
                !(_, rssT) =
                  _bcCont (traverse rqBulkCont rqs0) (vvToArray sn rss0) 0
            in  g rss1 rssT)
          (VecBuilder (rqs rqs1))
{-# INLINE batchRequest #-}

-- | Given an allocator function in any 'Functor', run a 'Batching' computation.
runBatching
  :: Functor f
  => (forall n. Vec n rq -> f (Vec n rs))
  -> Batching rq rs a -> f a
runBatching f (Batching go) = go
  (\n q rqs -> q . vvFromVec <$> f (vbToVec' n rqs))
  sintVal
  (const id)
  nil
{-# INLINE runBatching #-}

-- | Like 'runBatching', but without a 'Functor' (or implicitly in 'Identity').
runBatching_ :: (forall n. Vec n rq -> Vec n rs) -> Batching rq rs a -> a
runBatching_ f = runIdentity . runBatching (Identity . f)
{-# INLINE runBatching_ #-}

-- TODO(awpr): consider adding a Batched monad that supports many batches of
-- requests:
--
-- type Batched rq rs a = Free (Batching rq rs) a
--
-- -- Use ApplicativeDo inside this to group requests into a batch.
-- batch :: Batching rq rs a -> Batched rq rs a
--
-- -- Note this requires Monad rather than just Functor, since it can have
-- multiple batches.
-- runBatched
--   :: Monad f
--   => (forall n. KnownNat n => Vec n rq -> f (Vec n rs))
--   -> Batched rq rs a -> f a