diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## 0.0.4.0
+
+* Add documentation and functions to `Bluefin.Compound`
+
+## 0.0.3.0
+
+* Add `Bluefin.Reader`
+
+## 0.0.2.0
+
+* Add `Bluefin.StateSource`
+
 ## 0.0.1.0
 
 * Add `Bluefin.Writer`
diff --git a/bluefin.cabal b/bluefin.cabal
--- a/bluefin.cabal
+++ b/bluefin.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               bluefin
-version:            0.0.3.0
+version:            0.0.4.0
 license:            MIT
 license-file:       LICENSE
 author:             Tom Ellis
@@ -34,6 +34,6 @@
       Bluefin.Stream,
       Bluefin.Writer,
     build-depends:
-      bluefin-internal >= 0.0.3 && < 0.1
+      bluefin-internal >= 0.0.4 && < 0.1
     hs-source-dirs:   src
     default-language: Haskell2010
diff --git a/src/Bluefin.hs b/src/Bluefin.hs
--- a/src/Bluefin.hs
+++ b/src/Bluefin.hs
@@ -1,8 +1,8 @@
 module Bluefin
   ( -- * In brief
 
-    -- | Bluefin is an effect system which allows you, though
-    -- value-level handles, to freely mix a variety of effects
+    -- | Bluefin is an effect system which allows you to freely mix a
+    -- variety of effects, accessed though value-level handles,
     -- including
     --
     --  * "Bluefin.EarlyReturn", for early return
@@ -88,25 +88,26 @@
     -- ** Effect scoping
 
     -- | Bluefin's use of the type system is very similar to
-    -- 'Control.Monad.ST': it ensures that a handle can never escape
+    -- "Control.Monad.ST": it ensures that a handle can never escape
     -- the scope of its handler.  That is, once the handler has
     -- finished running there is no way you can use the handle
     -- anymore.
 
     -- ** Type signatures
 
-    -- | Bluefin type signatures follow a common pattern which looks
-    -- like
+    -- | The type signatures of Bluefin functions follow a common
+    -- pattern which looks like
     --
     -- @
     -- (e1 :> es, ...) -> \<Handle\> e1 -> ... -> Eff es r
     -- @
     --
-    --
-    -- Consider the example below, @incrementReadLine@, which reads
-    -- integers from standard input and accumulates them into a state.
-    -- It returns when it reads the input integer @0@ and it throws an
-    -- exception if it encounters an input line it cannot parse.
+    -- Here @\<Handle\>@ could be, for example, @State Int@,
+    -- @Exception String@ or @IOE@.  Consider the example below,
+    -- @incrementReadLine@, which reads integers from standard input
+    -- and accumulates them into a state.  It returns when it reads
+    -- the input integer @0@ and it throws an exception if it
+    -- encounters an input line it cannot parse.
     --
     -- Firstly, let's look at the arguments, which are all handles to
     -- Bluefin effects.  There is a state handle, an exception handle,
@@ -144,7 +145,7 @@
     -- incrementReadLine state exception io = do
     --   'Bluefin.Jump.withJump' $ \\break -> 'Control.Monad.forever' $ do
     --     line <- 'Bluefin.IO.effIO' io getLine
-    --     i <- case 'Text.Maybe.readMaybe' line of
+    --     i <- case 'Text.Read.readMaybe' line of
     --       Nothing ->
     --         'Bluefin.Exception.throw' exception ("Couldn't read: " ++ line)
     --       Just i ->
@@ -160,9 +161,20 @@
     -- must be handled by a corresponding handler, for example
     -- 'Bluefin.State.runState' for the state effect,
     -- 'Bluefin.Exception.try' for the exception effect and
-    -- 'Bluefin.Eff.runEff' for the @IO@ effect.
+    -- 'Bluefin.Eff.runEff' for the @IO@ effect.  The type signatures
+    -- of handlers also follow a common pattern, which looks like
     --
     -- @
+    -- (forall e. \<Handle\> e -> Eff (e :& es) a) -> Eff es r
+    -- @
+    --
+    -- This means that the effect @e@, corresponding to the handle
+    -- @\<Handle\> e@, has been handled and removed from the set of
+    -- remaining effects, @es@.  (The signatures for @runEff@ and
+    -- @runPureEff@ are slightly different because they remove all
+    -- effects.)
+    --
+    -- @
     -- runIncrementReadLine :: IO (Either String Int)
     -- runIncrementReadLine = 'Bluefin.Eff.runEff' $ \\io -> do
     --   'Bluefin.Exception.try' $ \\exception -> do
@@ -255,6 +267,10 @@
     -- default after adding inferred type signatures.)
     --
     -- * Writing a handler often requires an explicit type signature.
+
+    -- * Creating your own effects
+
+    -- | See "Bluefin.Compound".
 
     -- * Example
 
diff --git a/src/Bluefin/Compound.hs b/src/Bluefin/Compound.hs
--- a/src/Bluefin/Compound.hs
+++ b/src/Bluefin/Compound.hs
@@ -1,14 +1,470 @@
 module Bluefin.Compound
-  ( -- | @Compound@ allows combining two effects into one, for
-    -- encapsulation.  It is not documented yet.
+  ( -- * Creating your own effects
 
-    -- * Handle
-    Compound,
+    -- ** Wrap a single effect
 
-    -- * Handler
-    runCompound,
+    -- | Because in Bluefin everything happens at the value level,
+    -- creating your own effects is equivalent to creating your own
+    -- data types.  We just use the techniques we know and love from
+    -- Haskell!  For example, if I want to make a "counter" effect
+    -- that allows me to increment a counter then I can wrap a @State@
+    -- handle in a newtype:
+    --
+    -- @
+    -- newtype Counter1 e = MkCounter1 (State Int e)
+    --
+    -- incCounter1 :: (e :> es) => Counter1 e -> Eff es ()
+    -- incCounter1 (MkCounter1 st) = modify st (+ 1)
+    --
+    -- runCounter1 ::
+    --   (forall e. Counter1 e -> Eff (e :& es) r) ->
+    --   Eff es Int
+    -- runCounter1 k =
+    --   evalState 0 $ \\st -> do
+    --     _ <- k (MkCounter1 st)
+    --     get st
+    -- @
+    --
+    -- Running the handler tells me the number of times I incremented
+    -- the counter.
+    --
+    -- @
+    -- exampleCounter1 :: Int
+    -- exampleCounter1 = runPureEff $ runCounter1 $ \\c ->
+    --   incCounter1 c
+    --   incCounter1 c
+    --   incCounter1 c
+    -- @
+    --
+    -- @
+    -- >>> exampeleCounter1
+    -- 3
+    -- @
 
-    -- * Effectful operations
+    -- ** Wrap multiple effects, first attempt
+
+    -- | If we want to wrap multiple effects then we can use the
+    -- normal approach we use to wrap multiple values into a single
+    -- value: define a new data type with multiple fields.  There's a
+    -- caveat to this approach, but before we address the caveat let's
+    -- see the approach in action.  Here we define a new handle,
+    -- @Counter2@, that contains a @State@ and @Exception@ handle
+    -- within it.  That allows us to increment the counter and throw
+    -- an exception when we hit a limit.
+    --
+    -- @
+    -- data Counter2 e1 e2 = MkCounter2 (State Int e1) (Exception () e2)
+    --
+    -- incCounter2 :: (e1 :> es, e2 :> es) => Counter2 e1 e2 -> Eff es ()
+    -- incCounter2 (MkCounter2 st ex) = do
+    --   count <- get st
+    --   when (count >= 10) $
+    --     throw ex ()
+    --   put st (count + 1)
+    --
+    -- runCounter2 ::
+    --   (forall e1 e2. Counter2 e1 e2 -> Eff (e2 :& e1 :& es) r) ->
+    --   Eff es Int
+    -- runCounter2 k =
+    --   evalState 0 $ \\st -> do
+    --     _ \<- try $ \\ex -> do
+    --       k (MkCounter2 st ex)
+    --     get st
+    -- @
+    --
+    -- We can see that attempting to increment the counter fovever
+    -- bails out when we reach the limit.
+    --
+    -- @
+    -- exampleCounter2 :: Int
+    -- exampleCounter2 = runPureEff $ runCounter2 $ \\c ->
+    --   forever $
+    --     incCounter2 c
+    -- @
+    --
+    -- @
+    -- >>> exampleCounter2
+    -- 10
+    -- @
+    --
+    -- The flaw of this approach is that you expose one effect
+    -- parameter for each handle in the data type.  That's rather
+    -- cumbersome!  We can do better.
+
+    -- ** Wrap multiple effects, a better approach
+
+    -- | We can avoid exposing multiple effect parameters and just
+    -- expose a single one.  To make this work we have to define our
+    -- handler in a slightly different way.  Firstly we apply
+    -- @useImplIn@ to the effectful operation @k@ and secondly we
+    -- apply @mapHandle@ to each of the handles out of which we create
+    -- our compound handle.  Everything else remains the same.
+    --
+    -- @
+    -- data Counter3 e = MkCounter3 (State Int e) (Exception () e)
+    --
+    -- incCounter3 :: (e :> es) => Counter3 e -> Eff es ()
+    -- incCounter3 (MkCounter3 st ex) = do
+    --   count <- get st
+    --   when (count >= 10) $
+    --     throw ex ()
+    --   put st (count + 1)
+    --
+    -- runCounter3 ::
+    --   (forall e. Counter3 e -> Eff (e :& es) r) ->
+    --   Eff es Int
+    -- runCounter3 k =
+    --   evalState 0 $ \\st -> do
+    --     _ \<- try $ \\ex -> do
+    --       useImplIn k (MkCounter3 (mapHandle st) (mapHandle ex))
+    --     get st
+    -- @
+    --
+    -- The example works as before:
+    --
+    -- @
+    -- exampleCounter3 :: Int
+    -- exampleCounter3 = runPureEff $ runCounter3 $ \\c ->
+    --   forever $
+    --     incCounter3 c
+    -- @
+    --
+    -- @
+    -- >>> exampleCounter3
+    -- 10
+    -- @
+
+    -- ** Wrap multiple effects, don't handle them all
+
+    -- | So far our handlers have handled all the effects that are
+    -- found within our compound effect. We don't have to do that
+    -- though: we can leave some of the effects unhandled to be
+    -- handled by a different handler at a higher level.  Let's extend
+    -- our example with a @Stream@ effect.  Whenever we ask to
+    -- increment the counter, and it is currently an even number, then
+    -- we yield a message about that.  Additionally, there's a new
+    -- operation @getCounter4@ which allows us to yield a message
+    -- whilst returning the value of the counter.
+    --
+    -- @
+    -- data Counter4 e
+    --   = MkCounter4 (State Int e) (Exception () e) (Stream String e)
+    --
+    -- incCounter4 :: (e :> es) => Counter4 e -> Eff es ()
+    -- incCounter4 (MkCounter4 st ex y) = do
+    --   count <- get st
+    --
+    --   when (even count) $
+    --     yield y "Count was even"
+    --
+    --   when (count >= 10) $
+    --     throw ex ()
+    --
+    --   put st (count + 1)
+    --
+    -- getCounter4 :: (e :> es) => Counter4 e -> String -> Eff es Int
+    -- getCounter4 (MkCounter4 st _ y) msg = do
+    --   yield y msg
+    --   get st
+    --
+    -- runCounter4 ::
+    --   (e1 :> es) =>
+    --   Stream String e1 ->
+    --   (forall e. Counter4 e -> Eff (e :& es) r) ->
+    --   Eff es Int
+    -- runCounter4 y k =
+    --   evalState 0 $ \\st -> do
+    --     _ \<- try $ \\ex -> do
+    --       useImplIn k (MkCounter4 (mapHandle st) (mapHandle ex) (mapHandle y))
+    --     get st
+    -- @
+    --
+    -- @
+    -- exampleCounter4 :: ([String], Int)
+    -- exampleCounter4 = runPureEff $ yieldToList $ \\y -> do
+    --   runCounter4 y $ \\c -> do
+    --     incCounter4 c
+    --     incCounter4 c
+    --     n <- getCounter4 c "I'm getting the counter"
+    --     when (n == 2) $
+    --       yield y "n was 2, as expected"
+    -- @
+    --
+    -- @
+    -- >>> exampleCounter4
+    -- (["Count was even","I'm getting the counter","n was 2, as expected"],2)
+    -- @
+
+    -- ** Dynamic effects
+
+    -- | So far we've looked at "concrete" compound effects, that is,
+    -- new effects implemented in terms of specific other effects.  We
+    -- can also define dynamic effects, whose implementation is left
+    -- abstract, to be defined in the handler.  To do that we create a
+    -- handle that is a record of functions.  To run an effectful
+    -- operation we call one of the functions from the record.  We
+    -- define the record in the handler.  Here @incCounter5Impl@ and
+    -- @getCounter5Impl@ are exactly the same as @incCounter4@ and
+    -- @getCounter4@ were, they're just defined in the handler.  In
+    -- order to be used polymorphically, the actually effectful
+    -- functions we call, @incCounter5@ and @getCounter5@ are derived
+    -- from the record fields by applying @useImpl@.
+    --
+    -- @
+    -- data Counter5 e = MkCounter5
+    --   { incCounter5Impl :: Eff e (),
+    --     getCounter5Impl :: String -> Eff e Int
+    --   }
+    --
+    -- incCounter5 :: (e :> es) => Counter5 e -> Eff es ()
+    -- incCounter5 e = useImpl (incCounter5Impl e)
+    --
+    -- getCounter5 :: (e :> es) => Counter5 e -> String -> Eff es Int
+    -- getCounter5 e msg = useImpl (getCounter5Impl e msg)
+    --
+    -- runCounter5 ::
+    --   (e1 :> es) =>
+    --   Stream String e1 ->
+    --   (forall e. Counter5 e -> Eff (e :& es) r) ->
+    --   Eff es Int
+    -- runCounter5 y k =
+    --   evalState 0 $ \\st -> do
+    --     _ \<- try $ \\ex -> do
+    --       useImplIn
+    --         k
+    --         ( MkCounter5
+    --             { incCounter5Impl = do
+    --                 count <- get st
+    --
+    --                 when (even count) $
+    --                   yield y "Count was even"
+    --
+    --                 when (count >= 10) $
+    --                   throw ex ()
+    --
+    --                 put st (count + 1),
+    --               getCounter5Impl = \\msg -> do
+    --                 yield y msg
+    --                 get st
+    --             }
+    --         )
+    --     get st
+    -- @
+    --
+    -- The result is exactly the same as before
+    --
+    -- @
+    -- exampleCounter5 :: ([String], Int)
+    -- exampleCounter5 = runPureEff $ yieldToList $ \\y -> do
+    --   runCounter5 y $ \\c -> do
+    --     incCounter5 c
+    --     incCounter5 c
+    --     n <- getCounter5 c "I'm getting the counter"
+    --     when (n == 2) $
+    --       yield y "n was 2, as expected"
+    -- @
+    --
+    -- @
+    -- >>> exampleCounter5
+    -- (["Count was even","I'm getting the counter","n was 2, as expected"],2)
+    -- @
+
+    -- ** Combining concrete and dynamic effects
+
+    -- | We can also freely combine concrete and dynamic effects.  In
+    -- the following example, the @incCounter6@ effect is left
+    -- dynamic, and defined in the handler, whilst @getCounter6@ is
+    -- implemented in terms of concrete @State@ and @Stream@ effects.
+    --
+    -- @
+    -- data Counter6 e = MkCounter6
+    --   { incCounter6Impl :: Eff e (),
+    --     counter6State :: State Int e,
+    --     counter6Stream :: Stream String e
+    --   }
+    --
+    -- incCounter6 :: (e :> es) => Counter6 e -> Eff es ()
+    -- incCounter6 e = useImpl (incCounter6Impl e)
+    --
+    -- getCounter6 :: (e :> es) => Counter6 e -> String -> Eff es Int
+    -- getCounter6 (MkCounter6 _ st y) msg = do
+    --   yield y msg
+    --   get st
+    --
+    -- runCounter6 ::
+    --   (e1 :> es) =>
+    --   Stream String e1 ->
+    --   (forall e. Counter6 e -> Eff (e :& es) r) ->
+    --   Eff es Int
+    -- runCounter6 y k =
+    --   evalState 0 $ \\st -> do
+    --     _ \<- try $ \\ex -> do
+    --       useImplIn
+    --         k
+    --         ( MkCounter6
+    --             { incCounter6Impl = do
+    --                 count <- get st
+    --
+    --                 when (even count) $
+    --                   yield y "Count was even"
+    --
+    --                 when (count >= 10) $
+    --                   throw ex ()
+    --
+    --                 put st (count + 1),
+    --               counter6State = mapHandle st,
+    --               counter6Stream = mapHandle y
+    --             }
+    --         )
+    --     get st
+    -- @
+    --
+    -- Naturally, the result is the same.
+    --
+    -- @
+    -- exampleCounter6 :: ([String], Int)
+    -- exampleCounter6 = runPureEff $ yieldToList $ \\y -> do
+    --   runCounter6 y $ \\c -> do
+    --     incCounter6 c
+    --     incCounter6 c
+    --     n <- getCounter6 c "I'm getting the counter"
+    --     when (n == 2) $
+    --       yield y "n was 2, as expected"
+    -- @
+    --
+    -- @
+    -- >>> exampleCounter6
+    -- (["Count was even","I'm getting the counter","n was 2, as expected"],2)
+    -- @
+
+    -- ** A dynamic file system effect
+
+    -- | The @effectful@ library has [an example of a dynamic effect
+    -- for basic file system
+    -- access](https://hackage.haskell.org/package/effectful-core-2.2.1.0/docs/Effectful-Dispatch-Dynamic.html#g:2).
+    -- This is what it looks like in Bluefin.  We start by defining a
+    -- record of effectful operations.
+    --
+    -- @
+    -- data FileSystem es = MkFileSystem
+    --   { readFileImpl :: FilePath -> Eff es String,
+    --     writeFileImpl :: FilePath -> String -> Eff es ()
+    --   }
+    --
+    -- readFile :: (e :> es) => FileSystem e -> FilePath -> Eff es String
+    -- readFile fs filepath = useImpl (readFileImpl fs filepath)
+    --
+    -- writeFile :: (e :> es) => FileSystem e -> FilePath -> String -> Eff es ()
+    -- writeFile fs filepath contents = useImpl (writeFileImpl fs filepath contents)
+    -- @
+    --
+    -- We can make a pure handler that simulates reading and writing
+    -- to a file system by storing file contents in an association
+    -- list.
+    --
+    -- @
+    -- runFileSystemPure ::
+    --   (e1 :> es) =>
+    --   Exception String e1 ->
+    --   [(FilePath, String)] ->
+    --   (forall e2. FileSystem e2 -> Eff (e2 :& es) r) ->
+    --   Eff es r
+    -- runFileSystemPure ex fs0 k =
+    --   evalState fs0 $ \\fs ->
+    --     useImplIn
+    --       k
+    --       MkFileSystem
+    --         { readFileImpl = \\filepath -> do
+    --             fs' <- get fs
+    --             case lookup filepath fs' of
+    --               Nothing ->
+    --                 throw ex ("File not found: " <> filepath)
+    --               Just s -> pure s,
+    --           writeFileImpl = \\filepath contents ->
+    --             modify fs ((filepath, contents) :)
+    --         }
+    -- @
+    --
+    -- Or we can make a handler that actually performs IO operations
+    -- against a real file system.
+    --
+    -- @
+    -- runFileSystemIO ::
+    --   forall e1 e2 es r.
+    --   (e1 :> es, e2 :> es) =>
+    --   Exception String e1 ->
+    --   IOE e2 ->
+    --   (forall e. FileSystem e -> Eff (e :& es) r) ->
+    --   Eff es r
+    -- runFileSystemIO ex io k =
+    --   useImplIn
+    --     k
+    --     MkFileSystem
+    --       { readFileImpl =
+    --           adapt . Prelude.readFile,
+    --         writeFileImpl =
+    --           \\filepath -> adapt . Prelude.writeFile filepath
+    --       }
+    --   where
+    --     adapt :: (e1 :> ess, e2 :> ess) => IO a -> Eff ess a
+    --     adapt m =
+    --       effIO io (Control.Exception.try @IOException m) >>= \\case
+    --         Left e -> throw ex (show e)
+    --         Right r -> pure r
+    -- @
+    --
+    -- We can use the @FileSystem@ effect to define an action which
+    -- does some file system operations.
+    --
+    -- @
+    -- action :: (e :> es) => FileSystem e -> Eff es String
+    -- action fs = do
+    --   file <- readFile fs "\/dev\/null"
+    --   when (length file == 0) $ do
+    --     writeFile fs "\/tmp\/bluefin" "Hello!"
+    --   readFile fs "\/tmp\/doesn't exist"
+    -- @
+    --
+    -- and we can run it purely, against a simulated file system
+    --
+    -- @
+    -- exampleRunFileSystemPure :: Either String String
+    -- exampleRunFileSystemPure = runPureEff $ try $ \\ex ->
+    --   runFileSystemPure ex [("\/dev\/null", "")] action
+    -- @
+    --
+    -- @
+    -- >>> exampleRunFileSystemPure
+    -- Left "File not found: \/tmp\/doesn't exist"
+    -- @
+    --
+    -- or against the real file system.
+    --
+    -- @
+    -- exampleRunFileSystemIO :: IO (Either String String)
+    -- exampleRunFileSystemIO = runEff $ \\io -> try $ \\ex ->
+    --   runFileSystemIO ex io action
+    -- @
+    --
+    -- @
+    -- >>> exampleRunFileSystemIO
+    -- Left "\/tmp\/doesn't exist: openFile: does not exist (No such file or directory)"
+    -- \$ cat \/tmp\/bluefin
+    -- Hello!
+    -- @
+
+    -- * Functions for making compound effects
+
+    Handle (mapHandle),
+    useImpl,
+    useImplIn,
+
+    -- * Deprecated
+
+    -- | Do not use.  Will be removed in a future version.
+
+    Compound,
+    runCompound,
     withCompound,
   )
 where
