packages feed

boombox (empty) → 0.0

raw patch · 12 files changed

+527/−0 lines, 12 filesdep +basedep +bytestringdep +comonadsetup-changed

Dependencies added: base, bytestring, comonad, semigroupoids, semigroups, transformers

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015, Fumiaki Kinoshita
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Fumiaki Kinoshita nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple
+main = defaultMain
+ boombox.cabal view
@@ -0,0 +1,34 @@+-- Initial tapes.cabal generated by cabal init.  For further documentation,
+--  see http://haskell.org/cabal/users-guide/
+
+name:                boombox
+version:             0.0
+synopsis:            Chronokinetic stream sources and incremental consumers
+-- description:
+homepage:            https://github.com/fumieval/boombox
+license:             BSD3
+license-file:        LICENSE
+author:              Fumiaki Kinoshita
+maintainer:          fumiexcel@gmail.com
+-- copyright:
+category:            Data
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Data.Boombox
+    , Data.Boombox.Async
+    , Data.Boombox.Boombox
+    , Data.Boombox.Combinators
+    , Data.Boombox.Extra
+    , Data.Boombox.Head
+    , Data.Boombox.IO
+    , Data.Boombox.Player
+    , Data.Boombox.Tape
+  -- other-modules:
+  other-extensions:    BangPatterns, DeriveFunctor
+  build-depends:       base == 4.*, comonad, semigroups, semigroupoids, bytestring, transformers
+  hs-source-dirs:      src
+  ghc-options: -Wall -O2
+  default-language:    Haskell2010
+ src/Data/Boombox.hs view
@@ -0,0 +1,11 @@+module Data.Boombox (
+  module Data.Boombox.Tape
+  , module Data.Boombox.Boombox
+  , module Data.Boombox.Player
+  , module Data.Boombox.Combinators
+  , module Data.Boombox.Async) where
+import Data.Boombox.Tape
+import Data.Boombox.Player
+import Data.Boombox.Async
+import Data.Boombox.Combinators
+import Data.Boombox.Boombox
+ src/Data/Boombox/Async.hs view
@@ -0,0 +1,23 @@+module Data.Boombox.Async where
+
+import Control.Concurrent
+import Data.Boombox.Tape
+import Data.Foldable
+import Data.Function
+
+-- \ Merge multiple tapes in an asynchronous manner.
+asyncMergeTapes :: Functor w => [Tape w IO a] -> Tape w IO a
+asyncMergeTapes ts = Tape $ do
+  v <- newChan
+  let go k t = do
+        (a, w) <- unconsTape t
+        writeChan v (a, w, k)
+        takeMVar k >>= go k
+  for_ ts $ \t -> do
+    k <- newEmptyMVar
+    forkIO $ go k t
+  fix $ \self -> do
+    (a, w, k) <- readChan v
+    return (a, fmap (\cont -> Tape $ do
+      putMVar k $! cont
+      self) w)
+ src/Data/Boombox/Boombox.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE Rank2Types #-}
+module Data.Boombox.Boombox where
+
+import Control.Comonad
+import Data.Boombox.Tape
+import Data.Boombox.Player
+import Control.Monad.Trans.Class
+
+infix 6 @.$
+infix 6 @-$
+infixl 7 @->
+infixr 7 >-$
+infixl 8 >->
+
+-- | Feed a tape to a player and extract the final result.
+(@.$) :: (Comonad w, Monad m)
+  => Tape w m s
+  -> PlayerT w s m a
+  -> m a
+t0 @.$ p = connectDrive id (\_ _ -> return) [] t0 (runPlayerT p)
+{-# INLINE (@.$) #-}
+
+-- | Feed a tape to a player. It returns the leftover input, the remainder of the tape, and the result from the player.
+(@-$) :: (Comonad w, Monad m)
+  => Tape w m s
+  -> PlayerT w s m a
+  -> m ([s], Tape w m s, a)
+t0 @-$ p = connectDrive id (\a b c -> return (a, b, c)) [] t0 (runPlayerT p)
+{-# INLINE (@-$) #-}
+
+-- | @'Boombox' v w m a b@ is a transducer from @a@ to @b@ with monadic effect @m@, a comonadic control @v@ (outgoing) and @w@ (incoming).
+type Boombox v w m a = Tape w (PlayerT v a m)
+
+-- | Combine a tape with a boombox. The result will be synchronized with the boombox.
+(@->) :: (Comonad v, Functor w, Monad m) => Tape v m a -> Boombox v w m a b -> Tape w m b
+(@->) = composeWith id
+{-# INLINE (@->) #-}
+
+-- | Connect two boomboxes.
+(>->) :: (Comonad u, Comonad v, Functor w, Monad m)
+  => Boombox u v m a b
+  -> Boombox v w m b c
+  -> Boombox u w m a c
+(>->) = composeWith lift
+{-# INLINE (>->) #-}
+
+-- | Connect a boombox to a player.
+(>-$) :: (Comonad w, Monad m) => Boombox v w m a b -> PlayerT w b m r -> PlayerT v a m r
+t0 >-$ p0 = connectDrive lift (\_ _ -> return) [] t0 (runPlayerT p0)
+{-# INLINE (>-$) #-}
+
+composeWith :: (Comonad v, Functor w, Monad m, Functor n)
+  => (forall x. n x -> m x)
+  -> Tape v m a
+  -> Boombox v w n a b
+  -> Tape w m b
+composeWith trans = loop [] where
+  loop lo t (Tape m) = Tape $ connectDrive trans
+    (\lo' t' (a, w) -> return (a, loop lo' t' <$> w)) lo t (runPlayerT m)
+{-# INLINE composeWith #-}
+
+connectDrive :: (Comonad w, Monad m)
+  => (forall x. n x -> m x)
+  -> ([s] -> Tape w m s -> a -> m r)
+  -> [s]
+  -> Tape w m s
+  -> Drive w s n a
+  -> m r
+connectDrive td cont = loop where
+  loop lo t d = case d of
+    Done a -> cont lo t a
+    Partial f -> case lo of
+      [] -> do
+        (a, w) <- unconsTape t
+        loop [] (extract w) (f a)
+      (x:xs) -> loop xs t (f x)
+    Leftover s k -> loop (s : lo) t k
+    Eff m -> td m >>= loop lo t
+    Cont m -> do
+      (a, w) <- unconsTape t
+      m $ extend (loop lo . yield a) w
+{-# INLINE connectDrive #-}
+ src/Data/Boombox/Combinators.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE LambdaCase #-}
+module Data.Boombox.Combinators where
+
+foldMFrom :: Monad m => m (Maybe s) -> (r -> s -> m r) -> r -> m r
+foldMFrom m f r = m >>= \case
+  Just s -> f r s >>= foldMFrom m f
+  Nothing -> return r
+
+foldlFrom :: Monad m => m (Maybe s) -> (r -> s -> r) -> r -> m r
+foldlFrom m f r = m >>= \case
+  Just s -> foldlFrom m f $! f r s
+  Nothing -> return r
+
+traverseFrom_ :: Monad m => m (Maybe s) -> (s -> m r) -> m ()
+traverseFrom_ m k = m >>= \case
+  Just s -> k s >> traverseFrom_ m k
+  Nothing -> return ()
+ src/Data/Boombox/Extra.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE LambdaCase #-}
+module Data.Boombox.Extra where
+import Data.Boombox
+import Prelude hiding (takeWhile, dropWhile, lines)
+import qualified Data.ByteString as BS
+import Control.Comonad
+import Data.Functor.Identity
+
+-- | @peek ≡ lookAhead await@
+peek :: PlayerT w a m a
+peek = await >>= \a -> a <$ leftover a
+
+takeWhile :: (a -> Bool) -> PlayerT w a m [a]
+takeWhile p = do
+  a <- await
+  if p a
+    then (a:) <$> takeWhile p
+    else leftover a >> return []
+
+dropWhile :: (a -> Bool) -> PlayerT w a m ()
+dropWhile p = do
+  a <- await
+  if p a
+    then dropWhile p
+    else leftover a
+
+lines :: Comonad w => Boombox w Identity IO (Maybe BS.ByteString) (Maybe BS.ByteString)
+lines = Tape (go []) where
+  go ls = await >>= \case
+    Just c -> do
+      let (l, r) = BS.break (==10) c
+      if BS.null r
+        then go (l : ls)
+        else return (Just $ BS.concat $ reverse $ l : ls, pure
+            $ Tape $ leftover (Just (BS.tail r)) >> go [])
+    Nothing -> return $ case ls of
+      [] -> (Nothing, pure $ Tape $ go [])
+      _ -> (Just (BS.concat (reverse ls)), pure $ Tape $ go [])
+ src/Data/Boombox/Head.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE DeriveFunctor #-}
+module Data.Boombox.Head where
+import Data.Boombox.Player
+import Data.Boombox.Tape
+import Control.Comonad
+import Control.Applicative
+
+-- | 'Head' is a Store-like comonad which handles seeking.
+data Head i a = Head !i (Maybe i -> a) deriving Functor
+
+instance Comonad (Head i) where
+  extract (Head _ f) = f Nothing
+  extend k (Head i f) = Head i $ \m -> k $ Head (maybe i id m) f
+
+instance Ord i => Chronological (Head i) where
+  coincidence (Head i f) (Head j g) = case compare i j of
+    EQ -> Simultaneous (Head i (liftA2 (,) f g))
+    LT -> LeftFirst
+    GT -> RightFirst
+
+-- | Seek to an arbitrary position.
+seeksTape :: Monad m => (i -> Maybe i) -> Tape (Head i) m a -> Tape (Head i) m a
+seeksTape t (Tape m) = Tape $ m >>= \(_, Head i f) -> unconsTape (f (t i))
+
+-- | Get the current offset.
+posP :: PlayerT (Head i) s m i
+posP = control $ \(Head i f) -> (f Nothing, i)
+
+-- | Apply the given function to the current offset and jump to the resulting offset.
+seeksP :: (i -> Maybe i) -> PlayerT (Head i) s m ()
+seeksP t = control $ \(Head i f) -> (f (t i), ())
+
+-- | Seek to the given offset.
+seekP :: i -> PlayerT (Head i) s m ()
+seekP i = seeksP (const (Just i))
+ src/Data/Boombox/IO.hs view
@@ -0,0 +1,25 @@+module Data.Boombox.IO where
+
+import Data.Boombox.Tape
+import Data.Boombox.Head
+import Control.Monad.IO.Class
+import qualified System.IO as IO
+import qualified Data.ByteString as BS
+import Data.Int
+
+hGetContentsN :: MonadIO m => Int -> IO.Handle -> Tape (Head Int64) m (Maybe BS.ByteString)
+hGetContentsN n h = go 0 where
+  go i = Tape $ do
+    c <- liftIO $ BS.hGetSome h n
+    let l = BS.length c
+    if l <= 0
+      then return (Nothing, Head i $ maybe (go i) go)
+      else return (Just c, Head i $ maybe
+        (go (i + fromIntegral l))
+        (\j -> Tape $ liftIO (IO.hSeek h IO.AbsoluteSeek (fromIntegral j)) >> unconsTape (go j)))
+
+hGetContents :: MonadIO m => IO.Handle -> Tape (Head Int64) m (Maybe BS.ByteString)
+hGetContents = hGetContentsN 4080
+
+readFile :: MonadIO m => FilePath -> Tape (Head Int64) m (Maybe BS.ByteString)
+readFile path = effect $ hGetContents <$> liftIO (IO.openFile path IO.ReadMode)
+ src/Data/Boombox/Player.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE Rank2Types, LambdaCase, BangPatterns, DeriveFunctor, ExistentialQuantification #-}
+{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleContexts, FlexibleInstances #-}
+module Data.Boombox.Player where
+import Control.Monad
+import Control.Monad.Trans.Class
+import Control.Monad.IO.Class
+import Control.Applicative
+
+data Drive w s m a = Done a
+  | Partial (s -> Drive w s m a)
+  | Leftover s (Drive w s m a)
+  | Eff (m (Drive w s m a))
+  | Cont (forall r. w (Drive w s m a -> r) -> r)
+
+-- | @'Player' w s m a@ is a monadic consumer of a stream of @s@.
+-- 'Player' may send a control signal parameterized by @w@; the control surface of the producer
+-- (usually 'Tape') should match it.
+newtype PlayerT w s m a = PlayerT { unPlayerT :: forall r. (a -> Drive w s m r) -> Drive w s m r }
+
+instance Functor (PlayerT w s m) where
+  fmap f m = PlayerT $ \cs -> unPlayerT m (cs . f)
+
+instance Applicative (PlayerT w s m) where
+  pure = return
+  {-# INLINE pure #-}
+  (<*>) = ap
+  {-# INLINE (<*>) #-}
+
+instance Monad (PlayerT w s m) where
+  return a = PlayerT $ \cs -> cs a
+  m >>= k = PlayerT $ \cs -> unPlayerT m $ \a -> unPlayerT (k a) cs
+
+instance MonadTrans (PlayerT w s) where
+  lift m = PlayerT $ \cs -> Eff $ fmap cs m
+
+instance (MonadIO m) => MonadIO (PlayerT w s m) where
+  liftIO m = PlayerT $ \cs -> Eff $ fmap cs (liftIO m)
+
+instance Monoid a => Monoid (PlayerT w s m a) where
+  mempty = pure mempty
+  {-# INLINE mempty #-}
+  mappend = liftA2 mappend
+  {-# INLINE mappend #-}
+
+runPlayerT :: PlayerT w s m a -> Drive w s m a
+runPlayerT m = unPlayerT m Done
+
+-- | Send a control signal.
+control :: (forall a. w a -> (a, b)) -> PlayerT w s m b
+control k = PlayerT $ \cs -> Cont $ \wcont -> case k wcont of
+  (cont, b) -> cont (cs b)
+
+-- | Consume a value.
+await :: PlayerT w s m s
+await = PlayerT Partial
+{-# INLINABLE await #-}
+
+-- | Push a leftover back.
+leftover :: s -> PlayerT w s m ()
+leftover s = PlayerT $ \cs -> Leftover s (cs ())
+{-# INLINABLE leftover #-}
+
+-- | Put some leftovers.
+leftovers :: Foldable f => f s -> PlayerT w s m ()
+leftovers xs = PlayerT $ \cs -> foldr Leftover (cs ()) xs
+{-# INLINE leftovers #-}
+
+-- | Run a 'PlayerT' action without consuming any input.
+lookAhead :: (Functor w, Functor m) => PlayerT w s m a -> PlayerT w s m a
+lookAhead pl = PlayerT $ \cs -> go cs [] [] (unPlayerT pl Done) where
+  go cs l (x:xs) (Partial f) = go cs l xs (f x)
+  go cs l [] (Partial f) = Partial $ \x -> go cs (x : l) [] (f x)
+  go cs l xs (Leftover x k) = go cs l (x:xs) k
+  go cs l _ (Done a) = foldr Leftover (cs a) l
+  go cs l xs (Eff m) = Eff $ fmap (go cs l xs) m
+  go cs l xs (Cont m) = Cont $ m . fmap (. go cs l xs)
+ src/Data/Boombox/Tape.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
+{-# LANGUAGE Rank2Types, ScopedTypeVariables #-}
+{-# LANGUAGE DeriveTraversable #-}
+{-# LANGUAGE FlexibleContexts, FlexibleInstances, UndecidableInstances #-}
+module Data.Boombox.Tape (Tape(..)
+  -- * Consuming tapes
+  , headTape
+  , cueTape
+  -- * Constructing tapes
+  , yield
+  , yieldMany
+  , effect
+  , repeater
+  -- * Transforming tapes
+  , flattenTape
+  , filterTape
+  , foldTape
+  , hoistTransTape
+  , hoistTape
+  , transTape
+  , controlTape
+  , pushBack
+  , intercept
+  -- * Time series
+  , Chronological(..)
+  , EventOrder(..)
+  ) where
+
+import Control.Category
+import Control.Applicative
+import Data.Functor.Apply
+import Data.Functor.Identity
+import Prelude hiding (id, (.))
+import Control.Comonad.Env
+import Control.Comonad.Store
+import Control.Comonad.Traced hiding ((<>))
+import Data.Semigroup
+import Control.Arrow
+
+-- | @Tape w m a@ is a producer of values with a type @a@.
+-- It may cause effects @m@ and has a comonadic control @w@.
+newtype Tape w m a = Tape { unconsTape :: m (a, w (Tape w m a)) }
+  deriving (Functor)
+
+yield :: Applicative m => a -> w (Tape w m a) -> Tape w m a
+yield a w = Tape $ pure (a, w)
+{-# INLINE yield #-}
+
+effect :: Monad m => m (Tape w m a) -> Tape w m a
+effect m = Tape $ m >>= unconsTape
+{-# INLINE effect #-}
+
+-- | Build a tape that yields the same value, with the very same effect and exactly the same control.
+repeater :: (Functor m, Comonad w) => m (w a) -> Tape w m a
+repeater m = Tape $ fmap (\w -> (extract w, repeater m <$ w)) m
+
+-- | Take the first element of the tape.
+headTape :: Functor m => Tape w m a -> m a
+headTape = fmap fst . unconsTape
+
+-- | Denudate the control without dropping a value.
+cueTape :: (Comonad w, Applicative m) => Tape w m a -> m (w (Tape w m a))
+cueTape = fmap (\(a, w) -> extend (yield a) w) . unconsTape
+
+-- | Flatten a tape of 'Foldable' containers.
+flattenTape :: (Comonad w, Foldable f, Monad m) => Tape w m (f a) -> Tape w m a
+flattenTape = foldTape id
+{-# INLINE flattenTape #-}
+
+foldTape :: (Comonad w, Foldable f, Monad m) => (a -> f b) -> Tape w m a -> Tape w m b
+foldTape f = go where
+  go t = Tape $ unconsTape t >>= \(a, w) -> unconsTape $ yieldMany (f a) (fmap go w)
+{-# INLINE foldTape #-}
+
+filterTape :: (Comonad w, Monad m) => (a -> Bool) -> Tape w m a -> Tape w m a
+filterTape p = go where
+  go t = Tape $ unconsTape t >>= \(a, w) -> if p a then return (a, fmap go w) else unconsTape (go (extract w))
+
+yieldMany :: (Comonad w, Foldable f, Applicative m) => f a -> w (Tape w m a) -> Tape w m a
+yieldMany f w = extract $ foldr (extend . yield) w f
+{-# INLINE yieldMany #-}
+
+-- | Apply a monadic function to a tape.
+intercept :: (Functor w, Monad m) => (a -> m b) -> Tape w m a -> Tape w m b
+intercept k t = Tape $ unconsTape t >>= \(a, w) -> (\b -> (b, fmap (intercept k) w)) <$> k a
+
+hoistTransTape :: (Functor w, Functor n) => (forall x. v x -> w x) -> (forall x. m x -> n x) -> Tape v m a -> Tape w n a
+hoistTransTape s t = go where
+  go (Tape m) = Tape $ fmap (\(a, w) -> (a, fmap go (s w))) (t m)
+{-# INLINE hoistTransTape #-}
+
+-- | Apply natural transformation to the comonadic control surface.
+hoistTape :: (Functor w, Functor m) => (forall x. v x -> w x) -> Tape v m a -> Tape w m a
+hoistTape t = hoistTransTape t id
+{-# INLINE hoistTape #-}
+
+-- | Transform effects produced by the tape.
+transTape :: (Functor w, Functor n) => (forall x. m x -> n x) -> Tape w m a -> Tape w n a
+transTape = hoistTransTape id
+{-# INLINE transTape #-}
+
+-- | Operate on the control surface just once.
+controlTape :: Functor m => (w (Tape w m a) -> w (Tape w m a)) -> Tape w m a -> Tape w m a
+controlTape t (Tape m) = Tape $ fmap (second t) m
+
+-- | Push some values back to a tape.
+pushBack :: (Foldable f, Comonad w, Monad m) => f a -> Tape w m a -> Tape w m a
+pushBack f t = effect $ yieldMany f <$> cueTape t
+
+-- | 'Chronological' functor is like 'Apply', but the operation may fail due to a time lag.
+class Functor f => Chronological f where
+  coincidence :: f a -> f b -> EventOrder (f (a, b))
+
+data EventOrder a = Simultaneous a
+  | LeftFirst
+  | RightFirst
+  deriving Functor
+
+instance Chronological Identity where
+  coincidence (Identity a) (Identity b) = Simultaneous (Identity (a, b))
+
+instance Chronological ((->) i) where
+  coincidence f g = Simultaneous $ liftA2 (,) f g
+
+instance Ord i => Chronological ((,) i) where
+  coincidence (i, a) (j, b) = case compare i j of
+    EQ -> Simultaneous (i, (a, b))
+    LT -> LeftFirst
+    GT -> RightFirst
+
+instance (Ord i, Chronological w) => Chronological (EnvT i w) where
+  coincidence (EnvT i v) (EnvT j w) = case compare i j of
+    EQ -> EnvT i <$> coincidence v w
+    LT -> LeftFirst
+    GT -> RightFirst
+
+instance (Ord i, Chronological w) => Chronological (StoreT i w) where
+  coincidence (StoreT v i) (StoreT w j) = case compare i j of
+    EQ -> (\wfg -> StoreT (fmap (uncurry $ liftA2 (,)) wfg) i) <$> coincidence v w
+    LT -> LeftFirst
+    GT -> RightFirst
+
+instance Chronological w => Chronological (TracedT m w) where
+  coincidence (TracedT v) (TracedT w) = fmap (TracedT . fmap (uncurry $ liftA2 (,))) $ coincidence v w
+
+instance (Chronological w, Monad m, Semigroup a)
+  => Semigroup (Tape w m a) where
+    s <> t = Tape $ do
+      (a, v) <- unconsTape s
+      (b, w) <- unconsTape t
+      case coincidence v w of
+        Simultaneous u -> return (a <> b, fmap (uncurry (<>)) u)
+        LeftFirst -> return (a, fmap (<> t) v)
+        RightFirst -> return (b, fmap (s <>) w)