diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2016, Cale Gibbard
+
+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 Cale Gibbard 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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/category-printf.cabal b/category-printf.cabal
new file mode 100644
--- /dev/null
+++ b/category-printf.cabal
@@ -0,0 +1,34 @@
+name:                category-printf
+version:             0.1.0.0
+synopsis:            Highbrow approach to type-safe printf format specifications.
+description:         We use the co-Kleisli category for the comonad of
+                     functions out of a fixed monoid to implement a generic
+                     combinator library for type-safe format specifications.
+                     Works with pretty much anything that's a monoid, with
+                     specific support for String, Text (strict/lazy), and
+                     ByteString (strict/lazy). Credit to Daniel Patterson
+                     for introducing me to something that looked just enough
+                     like it ought to be the composition for a category, and
+                     Ryan Trinkle for the IsString instance. :)
+license:             BSD3
+license-file:        LICENSE
+author:              Cale Gibbard
+maintainer:          cgibbard@gmail.com
+category:            Text
+build-type:          Simple
+
+-- Constraint on the version of Cabal needed to build this package.
+cabal-version:       >=1.10
+
+library
+  exposed-modules:     Control.Category.Printf,
+                       Control.Category.Printf.String,
+                       Control.Category.Printf.Text.Strict,
+                       Control.Category.Printf.Text.Lazy,
+                       Control.Category.Printf.ByteString.Strict,
+                       Control.Category.Printf.ByteString.Lazy
+  
+  other-extensions:    OverloadedStrings, FlexibleInstances, TypeFamilies
+  build-depends:       base >=4.7 && <4.8, comonad >=5 && <5.1, text >=1.1 && <1.2, bytestring >=0.10 && <0.11
+  hs-source-dirs:      src
+  default-language:    Haskell2010
diff --git a/src/Control/Category/Printf.hs b/src/Control/Category/Printf.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Category/Printf.hs
@@ -0,0 +1,142 @@
+{-# LANGUAGE OverloadedStrings, FlexibleInstances, TypeFamilies #-}
+
+module Control.Category.Printf 
+       ( module Control.Category
+       , Format
+       , printfWith
+       , sprintf
+       , c
+       , i
+       , spliceWith
+       , s
+       , generalizeString
+       , intAtBase
+       , hex
+       , oct
+       , eFloat
+       , fFloat
+       , gFloat
+       , push
+       , dup
+       , swap
+       , skip
+       , apply
+       , apply2
+       ) where
+
+import Prelude hiding (id, (.))
+import Control.Arrow
+import Control.Category
+import Data.Monoid
+import Control.Comonad
+import Numeric
+
+import Data.String
+
+-- | Handy type synonym for the things we're working with.
+type Format m = Cokleisli ((->) m)
+
+-- | You should regard a value of type @Format m a b@ as something which explains how to write
+-- some element of the monoid @m@ (a "string" for our purposes), and which will change the type
+-- of printf from @a@ to @b@. For instance, something which adds a responsibility to provide an
+-- additional argument of type @t@ might have type @Format m a (t -> a)@, while a formatter which
+-- somehow absolves you of that responsibility would have type @Format m (t -> a) a@.
+
+-- | We can apply this to something like putStrLn to get a function for formatted printing.
+-- Typically you'll have @r = IO ()@, but that needn't be the case.
+printfWith :: (m -> r) -> Format m r b -> b
+printfWith printer f = runCokleisli f printer
+
+-- | If you just want to build a string / element of your monoid, we have @sprintf = printfWith id@
+sprintf :: Format m m b -> b
+sprintf = printfWith id
+
+instance (a ~ b, IsString s, Monoid s) => IsString (Cokleisli ((->) s) a b) where
+  fromString = c . fromString
+
+-- | Formatter for a constant string.
+c :: (Monoid m) => m -> Format m a a
+c x = Cokleisli (\k -> k x)
+
+-- | Inclusion of a string directly.
+i :: Format m a (m -> a)
+i = Cokleisli id
+
+-- | Given a way to turn a value of type t into a string, this builds a
+-- formatter which demands an additional argument of type t and splices it in.
+spliceWith :: (Monoid m) => (t -> m) -> Format m a (t -> a)
+spliceWith f = Cokleisli (\k n -> k (f n))
+
+-- | Splice in anything showable.
+s :: (Monoid s, IsString s, Show t) => Format s a (t -> a)
+s = spliceWith (fromString . show)
+
+-- | Generalizes the string type that a formatter uses by applying fromString internally.
+generalizeString :: (IsString s, Monoid s) => Format String a b -> Format s a b
+generalizeString f = Cokleisli (\k -> runCokleisli f (k . fromString))
+
+-- | Show an integral value using the given base, and using the provided function to determine how
+-- to display individual digits.
+intAtBase :: (Real t, Integral t, Show t, Monoid s, IsString s)
+          => t -> (Int -> Char) -> Format s a (t -> a)
+intAtBase b showDigit = generalizeString $
+  spliceWith (\n -> showSigned (showIntAtBase b showDigit) 0 n "")
+
+-- | Show an integral value in hexadecimal.
+hex :: (Integral t, Show t, Monoid s, IsString s) => Format s a (t -> a)
+hex = generalizeString $
+  spliceWith (\n -> showSigned showHex 0 n "")
+
+-- | Show an integral value in octal.
+oct :: (Integral t, Show t, Monoid s, IsString s) => Format s a (t -> a)
+oct = generalizeString $
+  spliceWith (\n -> showSigned showOct 0 n "")
+
+-- | Show a floating point value in exponential format. (e.g. 2.45e2, -1.5e-3)
+-- If `digs` is Nothing, the value is shown to full precision, if it is Just d then at most
+-- d digits after the decimal point are shown.
+eFloat :: (RealFloat t, Monoid s, IsString s) => Maybe Int -> Format s a (t -> a)
+eFloat digs = generalizeString $ spliceWith (\n -> showEFloat digs n "")
+
+-- | Show a floating point value in standard decimal format. (e.g. 245000, -0.0015)
+-- If `digs` is Nothing, the value is shown to full precision, if it is Just d then at most
+-- d digits after the decimal point are shown.
+fFloat :: (RealFloat t, Monoid s, IsString s) => Maybe Int -> Format s a (t -> a)
+fFloat digs = generalizeString $ spliceWith (\n -> showFFloat digs n "")
+
+-- | Show a floating point value using standard decimal notation for arguments whose absolute
+-- value lies between 0.1 and 9,999,999, and scientific notation otherwise. 
+-- If `digs` is Nothing, the value is shown to full precision, if it is Just d then at most
+-- d digits after the decimal point are shown.
+gFloat :: (RealFloat t, Monoid s, IsString s) => Maybe Int -> Format s a (t -> a)
+gFloat digs = generalizeString $ spliceWith (\n -> showGFloat digs n "")
+
+-- | We can use `arr` from the Arrow instance for Cokleisli w to produce formatters
+-- that manipulate the stack without printing. That is, we have
+-- 
+-- > arr :: (Monoid m) => (s -> s') -> Format m s s'
+
+-- | Push an argument onto the stack to be consumed by subsequent formatters.
+push :: Monoid m => t -> Format m (t -> a) a
+push x = arr (\k -> k x)
+
+-- | Duplicate an argument on the stack, making it available twice.
+dup :: Monoid m => Format m (t -> t -> a) (t -> a)
+dup = arr (\k x -> k x x)
+
+-- | Swap the next two arguments on the stack.
+swap :: Monoid m => Format m (t -> t' -> a) (t' -> t -> a)
+swap = arr (\k x y -> k y x)
+
+-- | Skip the next argument on the stack.
+skip :: Monoid m => Format m (t -> a) (t' -> t -> a)
+skip = arr (\k x -> k)
+
+-- | Apply a function to the argument on the top of the stack.
+apply :: Monoid m => (u -> v) -> Format m (v -> a) (u -> a)
+apply f = arr (\k x -> k (f x))
+
+-- | Apply a binary function to the top two arguments on the stack.
+apply2 :: Monoid m => (u -> v -> w) -> Format m (w -> a) (u -> v -> a)
+apply2 f = arr (\k x y -> k (f x y))
+
diff --git a/src/Control/Category/Printf/ByteString/Lazy.hs b/src/Control/Category/Printf/ByteString/Lazy.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Category/Printf/ByteString/Lazy.hs
@@ -0,0 +1,28 @@
+module Control.Category.Printf.ByteString.Lazy
+       ( module Control.Category.Printf
+       , printf
+       , printfLn
+       , hPrintf
+       , hPrintfLn
+       ) where
+
+import Prelude hiding (id, (.))
+import Control.Comonad
+import Control.Category.Printf
+import Data.ByteString.Lazy (ByteString)
+import qualified Data.ByteString.Lazy as B
+import qualified Data.ByteString.Lazy.Char8 as BC
+import System.IO (Handle)
+
+printf :: Format ByteString (IO ()) b -> b
+printf = printfWith B.putStr
+
+printfLn :: Format ByteString (IO ()) b -> b
+printfLn = printfWith BC.putStrLn 
+
+hPrintf :: Handle -> Format ByteString (IO ()) b -> b
+hPrintf h = printfWith (B.hPutStr h)
+
+hPrintfLn :: Handle -> Format ByteString (IO ()) b -> b
+hPrintfLn h = printfWith (BC.hPutStrLn h)
+
diff --git a/src/Control/Category/Printf/ByteString/Strict.hs b/src/Control/Category/Printf/ByteString/Strict.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Category/Printf/ByteString/Strict.hs
@@ -0,0 +1,28 @@
+module Control.Category.Printf.ByteString.Strict
+       ( module Control.Category.Printf
+       , printf
+       , printfLn
+       , hPrintf
+       , hPrintfLn
+       ) where
+
+import Prelude hiding (id, (.))
+import Control.Comonad
+import Control.Category.Printf
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as BC
+import System.IO (Handle)
+
+printf :: Format ByteString (IO ()) b -> b
+printf = printfWith B.putStr
+
+printfLn :: Format ByteString (IO ()) b -> b
+printfLn = printfWith BC.putStrLn 
+
+hPrintf :: Handle -> Format ByteString (IO ()) b -> b
+hPrintf h = printfWith (B.hPutStr h)
+
+hPrintfLn :: Handle -> Format ByteString (IO ()) b -> b
+hPrintfLn h = printfWith (BC.hPutStrLn h)
+
diff --git a/src/Control/Category/Printf/String.hs b/src/Control/Category/Printf/String.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Category/Printf/String.hs
@@ -0,0 +1,23 @@
+module Control.Category.Printf.String 
+       (module Control.Category.Printf
+       , printf
+       , printfLn
+       , hPrintf
+       , hPrintfLn
+       ) where
+
+import Control.Comonad
+import Control.Category.Printf
+import System.IO (Handle, hPutStr, hPutStrLn)
+
+printf :: Format String (IO ()) b -> b
+printf = printfWith putStr
+
+printfLn :: Format String (IO ()) b -> b
+printfLn = printfWith putStrLn
+
+hPrintf :: Handle -> Format String (IO ()) b -> b
+hPrintf h = printfWith (hPutStr h)
+
+hPrintfLn :: Handle -> Format String (IO ()) b -> b
+hPrintfLn h = printfWith (hPutStrLn h)
diff --git a/src/Control/Category/Printf/Text/Lazy.hs b/src/Control/Category/Printf/Text/Lazy.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Category/Printf/Text/Lazy.hs
@@ -0,0 +1,26 @@
+module Control.Category.Printf.Text.Lazy
+       ( module Control.Category.Printf
+       , printf
+       , printfLn
+       , hPrintf
+       , hPrintfLn
+       ) where
+
+import Prelude hiding (id, (.))
+import Control.Comonad
+import Control.Category.Printf
+import Data.Text.Lazy (Text)
+import qualified Data.Text.Lazy.IO as T
+import System.IO (Handle)
+
+printf :: Format Text (IO ()) b -> b
+printf = printfWith T.putStr
+
+printfLn :: Format Text (IO ()) b -> b
+printfLn = printfWith T.putStrLn 
+
+hPrintf :: Handle -> Format Text (IO ()) b -> b
+hPrintf h = printfWith (T.hPutStr h)
+
+hPrintfLn :: Handle -> Format Text (IO ()) b -> b
+hPrintfLn h = printfWith (T.hPutStrLn h)
diff --git a/src/Control/Category/Printf/Text/Strict.hs b/src/Control/Category/Printf/Text/Strict.hs
new file mode 100644
--- /dev/null
+++ b/src/Control/Category/Printf/Text/Strict.hs
@@ -0,0 +1,26 @@
+module Control.Category.Printf.Text.Strict
+       ( module Control.Category.Printf
+       , printf
+       , printfLn
+       , hPrintf
+       , hPrintfLn
+       ) where
+
+import Prelude hiding (id, (.))
+import Control.Comonad
+import Control.Category.Printf
+import Data.Text (Text)
+import qualified Data.Text.IO as T
+import System.IO (Handle)
+
+printf :: Format Text (IO ()) b -> b
+printf = printfWith T.putStr
+
+printfLn :: Format Text (IO ()) b -> b
+printfLn = printfWith T.putStrLn 
+
+hPrintf :: Handle -> Format Text (IO ()) b -> b
+hPrintf h = printfWith (T.hPutStr h)
+
+hPrintfLn :: Handle -> Format Text (IO ()) b -> b
+hPrintfLn h = printfWith (T.hPutStrLn h)
