diff --git a/ply-loader.cabal b/ply-loader.cabal
--- a/ply-loader.cabal
+++ b/ply-loader.cabal
@@ -1,5 +1,5 @@
 name:                ply-loader
-version:             0.1.1.2
+version:             0.2
 synopsis:            PLY file loader.
 
 description:         PLY is a lightweight file format for representing 3D
@@ -32,8 +32,8 @@
 
 library
   ghc-options:         -O2 -Wall
-  exposed-modules:     PLY.Data, PLY.Types, PLY.Conf, PLY.Internal.Parsers,
-                       PLY.Internal.StrictReplicate
+  exposed-modules:     PLY, PLY.Ascii, PLY.Conf, PLY.Types,
+                       PLY.Internal.Parsers, PLY.Internal.StrictReplicate
                        
   build-depends:       base >= 4.6 && < 5, 
                        attoparsec >= 0.10, 
@@ -43,7 +43,8 @@
                        vector >= 0.9,
                        filepath,
                        directory,
-                       parallel-io >= 0.3.2
+                       parallel-io >= 0.3.2,
+                       transformers
   hs-source-dirs:      src
   default-language:    Haskell2010
 
diff --git a/src/PLY.hs b/src/PLY.hs
new file mode 100644
--- /dev/null
+++ b/src/PLY.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE ScopedTypeVariables, BangPatterns #-}
+-- |The loading of a @ply@ file is broken down into two steps: header
+-- parsing, and data loading. The 'loadPLY' function will, if
+-- successful, return a data structure that may be queried to extract
+-- numeric data using 'loadElements' and 'loadElementsV3'. For example, 
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- > import Data.Vector.Storable (Vector)
+-- > import Linear.V3
+-- > import PLY
+-- >
+-- > loadVerts :: FilePath -> IO (Either String (Vector (V3 Float)))
+-- > loadVerts = loadElementsV3 "vertex"
+-- 
+-- To load all vertex data from a series of @ply@ files identified by
+-- a @.conf@ file, consider using,
+--
+-- > fromConf :: FilePath -> IO (Either String (Vector (V3 Float)))
+-- > fromConf = loadConfV3 "vertex"
+--
+module PLY (-- * Easy loading interface
+            loadElements, loadElementsV3, loadConfV3,
+
+            -- * Loading components
+            PLYData, loadHeader, preloadPly, 
+            loadPlyElements, loadPlyElementsV3) where
+import Control.Applicative
+import Control.Concurrent.ParallelIO (parallel)
+import Control.Monad ((>=>))
+import Control.Monad.Trans.Error
+import Data.Attoparsec.Char8
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Char8 as BC
+import Data.Either (partitionEithers)
+import Data.Vector (Vector)
+import qualified Data.Vector.Storable as VS
+import Linear
+import System.Directory (canonicalizePath)
+import System.FilePath (takeDirectory, (</>))
+
+import PLY.Ascii
+import PLY.Conf
+import PLY.Internal.Parsers (line, parseSkip, header)
+import PLY.Types
+
+type Header = (Format, [Element])
+
+-- |A PLY header and the associated raw data. Use 'loadElements' or
+-- 'loadElementsV3' to extract a particular element array.
+data PLYData = PLYData !ByteString !Header
+
+instance Show PLYData where
+  show (PLYData _ h) = "PLYData <bytes> " ++ show h
+
+-- Helper to ensure that that an 'Either' is strict in the argument to
+-- the data constructor. This is important to keep Vector operations
+-- flowing efficiently.
+strictE :: Either a b -> Either a b
+strictE l@(Left !_x) = l
+strictE r@(Right !_x) = r
+
+-- |Load a PLY header from a file.
+loadHeader :: FilePath -> IO (Either String PLYData)
+loadHeader = fmap preloadPly . BS.readFile
+
+-- |Attempt to parse a PLY file from the given bytes. If the PLY
+-- header is successfully parsed, the 'PLYData' value returned may be
+-- used with 'loadElements' and 'loadElementsV3' to extract data.
+preloadPly :: ByteString -> Either String PLYData
+preloadPly = aux . parse header
+  where aux (Fail _t ctxt msg) = Left $ "Parse failed: "++msg++" in "++show ctxt
+        aux (Partial _) = Left "Incomplete header"
+        aux (Done t r) = Right $ PLYData t r
+
+-- |@loadPlyElements elementName ply@ loads a 'Vector' of each vertex of
+-- the requested element array. If you are extracting 3D data,
+-- consider using 'loadPlyElementsV3'.
+loadPlyElements :: ByteString -> PLYData -> 
+                   Either String (Vector (Vector Scalar))
+loadPlyElements n (PLYData body (ASCII, ess)) = strictE $ go ess body
+  where go [] _ = Left "Unknown element"
+        go (e:es) b | elName e == n = parseOnly (parseASCII e) b
+                    | otherwise = go es $
+                                  parseSkip (count (elNum e) line *> pure ()) b
+loadPlyElements _ _ = error "Binary PLY is unsupported"
+{-# INLINABLE loadPlyElements #-}
+
+-- |@loadElements elementName plyFile@ loads a 'Vector' of each
+-- vertex of the requested element array from @plyFile@.
+loadElements :: ByteString -> FilePath
+             -> IO (Either String (Vector (Vector Scalar)))
+loadElements name file =
+  (preloadPly >=> loadPlyElements name) <$> BS.readFile file
+{-# INLINABLE loadElements #-}
+
+-- |Like 'loadPlyElements', but restricted to 3D vectors. When it can be
+-- used, this function is much more efficient than 'loadPlyElements'.
+loadPlyElementsV3 :: PLYType a => ByteString -> PLYData -> 
+                     Either String (VS.Vector (V3 a))
+loadPlyElementsV3 n (PLYData body (ASCII, ess)) = strictE $ go ess body
+  where go [] _ = Left "Unknown element"
+        go (e:es) b | elName e == n = parseOnly (parseASCIIv3 e) b
+                    | otherwise = go es $
+                                  parseSkip (count (elNum e) line *> pure ()) b
+loadPlyElementsV3 _ _ = error "Binary PLY is unsupported"
+{-# INLINABLE loadPlyElementsV3 #-}
+
+-- |Like 'loadElements', but restricted to 3D vectors. When it can
+-- be used, this function is much more efficient thatn
+-- 'loadPlyElements'.
+loadElementsV3 :: PLYType a => ByteString -> FilePath
+               -> IO (Either String (VS.Vector (V3 a)))
+loadElementsV3 name file =
+  (preloadPly >=> loadPlyElementsV3 name) <$> BS.readFile file
+{-# INLINABLE loadElementsV3 #-}
+
+type ErrorMsg a = Either String a
+
+-- |Load all meshes identified by a @.conf@ file in parallel, and
+-- transform vertex data into the coordinate frame specified by the
+-- @.conf@ file. The application @loadMeshesV3 confFile element@ loads
+-- @confFile@ to identify every @ply@ mesh to load. The @ply@ files
+-- are loaded from the same directory that contained the @.conf@ file,
+-- and the data associated with @element@ (e.g. @\"vertex\"@) is
+-- loaded, transformed, and concatenated from all the meshes.
+loadConfV3 :: forall a. (PLYType a, Fractional a, Conjugate a, RealFloat a)
+           => ByteString -> FilePath -> IO (Either String (VS.Vector (V3 a)))
+loadConfV3 element confFile = 
+  do dir <- takeDirectory <$> canonicalizePath confFile
+     runErrorT $
+       do c <- ErrorT $ parseConf <$> BS.readFile confFile
+          ErrorT $ checkConcat <$> loadAll dir c
+    where checkErrors :: [ErrorMsg b] -> ErrorMsg [b]
+          checkErrors xs = let (ls,rs) = partitionEithers xs
+                           in if null ls then Right rs else Left (unlines ls)
+          checkConcat :: [ErrorMsg (VS.Vector (V3 a))]
+                      -> ErrorMsg (VS.Vector (V3 a))
+          checkConcat = (fmap VS.concat $!) . checkErrors
+          loadMesh :: FilePath -> M44 a -> (ByteString, Transformation a)
+                   -> IO (ErrorMsg (VS.Vector (V3 a)))
+          loadMesh d _cam (f, (t,r)) = 
+            -- It is convenient to ignore the camera transformation so
+            -- that the object is at the origin.
+            let m = (^+^ fmap realToFrac t ) . rotate (conjugate (fmap realToFrac r))
+            in fmap (VS.map m) <$> loadElementsV3 element (d </> BC.unpack f)
+          loadAll :: FilePath -> Conf a -> IO ([ErrorMsg (VS.Vector (V3 a))])
+          loadAll dir (Conf (t,r) ms) = 
+            let cam = mkTransformation r t
+            in parallel $ map (loadMesh dir cam) ms
+{-# INLINABLE loadConfV3 #-}
diff --git a/src/PLY/Ascii.hs b/src/PLY/Ascii.hs
new file mode 100644
--- /dev/null
+++ b/src/PLY/Ascii.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+module PLY.Ascii where
+import Control.Applicative
+import Data.Attoparsec.Char8
+import Data.Vector (Vector)
+import qualified Data.Vector as V
+import qualified Data.Vector.Storable as VS
+import Linear.V3 (V3(..))
+
+import PLY.Internal.Parsers
+import PLY.Internal.StrictReplicate
+import PLY.Types
+
+parseASCII :: Element -> Parser (Vector (Vector Scalar))
+parseASCII e = replicateM' (elNum e)
+                           (skip *> (V.fromList <$> multiProps (elProps e)))
+
+parseASCIIv3 :: forall a. PLYType a => Element -> Parser (VS.Vector (V3 a))
+parseASCIIv3 (Element _ n ps@[_,_,_])
+  | all samePropType ps = replicateM' n (skip *> (V3 <$> p <*> p <*> p))
+  | otherwise = empty
+  where t = plyType (undefined::a)
+        p = unsafeUnwrap <$> (parseScalar t <* skipSpace)
+        samePropType (ScalarProperty t' _) = t == t'
+        samePropType (ListProperty _ _) = False
+parseASCIIv3 _ = empty
diff --git a/src/PLY/Conf.hs b/src/PLY/Conf.hs
--- a/src/PLY/Conf.hs
+++ b/src/PLY/Conf.hs
@@ -16,28 +16,29 @@
 -- |A @.conf@ file includes a base transformation matrix, and a
 -- list of meshes identified by their file name and a 'Transformation'
 -- to place geometry in a consistent coordinate frame.
-data Conf = Conf { camera :: Transformation Double 
-                 , meshes :: [(ByteString, Transformation Double)] }
-            deriving Show
+data Conf a = Conf { camera :: Transformation a
+                   , meshes :: [(ByteString, Transformation a)] }
+              deriving Show
 
 -- |Parse a 3D translation vector followed by a quaternion.
-transformation :: Parser (V3 Double, Quaternion Double)
+transformation :: Fractional a => Parser (V3 a, Quaternion a)
 transformation = (,) <$> vec <*> rotation
-  where vec = (\[x,y,z] -> V3 x y z) <$> count 3 (skipSpace *> double)
-        rotation = flip Quaternion <$> vec <*> (skipSpace *> double)
+  where vec = (\[x,y,z] -> V3 x y z) <$> count 3 (skipSpace *> double')
+        rotation = flip Quaternion <$> vec <*> (skipSpace *> double')
+        double' = realToFrac <$> double
         --rotation = Quaternion <$> (skipSpace *> double) <*> vec
         --rev (V3 x y z) = V3 z y x
 
 -- |Parse a mesh file specification.
-mesh :: Parser (ByteString, Transformation Double)
+mesh :: Fractional a => Parser (ByteString, Transformation a)
 mesh = (,) <$> ("bmesh " .*> word) <*> transformation
 
 -- |Parser for a Stanford .conf file.
-conf :: Parser Conf
+conf :: Fractional a => Parser (Conf a)
 conf = Conf <$> ("camera " .*> transformation <* skipSpace)
             <*> (skipSpace *> cybmesh *> many1 (skipSpace *> mesh))
   where cybmesh = skipMany ("mesh " .*> line) -- Not a PLY file!
 
 -- |Parse a Stanford .conf file.
-parseConf :: ByteString -> Either String Conf
+parseConf :: Fractional a => ByteString -> Either String (Conf a)
 parseConf = parseOnly conf
diff --git a/src/PLY/Data.hs b/src/PLY/Data.hs
deleted file mode 100644
--- a/src/PLY/Data.hs
+++ /dev/null
@@ -1,147 +0,0 @@
-{-# LANGUAGE ScopedTypeVariables, BangPatterns #-}
--- |The loading of a @ply@ file is broken down into two steps: header
--- parsing, and data loading. The 'loadPLY' function will, if
--- successful, return a data structure that may be queried to extract
--- numeric data using 'loadElements' and 'loadElementsV3'. For example, 
---
--- > {-# LANGUAGE OverloadedStrings #-}
--- > import Control.Monad ((>=>))
--- > import Data.ByteString (ByteString)
--- > import qualified Data.Vector.Storable as VS
--- > import Linear.V3
--- >
--- > loadVerts :: ByteString -> Either String (VS.Vector (V3 Float))
--- > loadVerts = loadPLY >=> loadElementsV3 "vertex"
--- 
--- To load all vertex data from a series of @ply@ files identified by
--- a @.conf@ file, consider using,
---
--- > loadConf :: FilePath -> IO (Either [String] (VS.Vector (V3 Float)))
--- > loadConf confFile = loadMeshesV3 confFile "vertex"
---
-module PLY.Data (PLYData, loadPLY, loadElements, loadElementsV3, 
-                 loadMeshesV3, loadHeader) where
-import Control.Applicative
-import Control.Concurrent.ParallelIO (parallel)
-import Data.Attoparsec.Char8
-import Data.ByteString (ByteString)
-import qualified Data.ByteString as BS
-import qualified Data.ByteString.Char8 as BC
-import Data.Either (partitionEithers)
-import Data.Vector (Vector)
-import qualified Data.Vector as V
-import qualified Data.Vector.Storable as VS
-import Linear
-import System.Directory (canonicalizePath)
-import System.FilePath (takeDirectory, (</>))
-
-import PLY.Conf
-import PLY.Internal.Parsers (line, parseSkip, skip, multiProps, 
-                             parseScalar, header)
-import PLY.Internal.StrictReplicate
-import PLY.Types
-
-type Header = (Format, [Element])
-
--- |A PLY header and the associated raw data. Use 'loadElements' or
--- 'loadElementsV3' to extract a particular element array.
-newtype PLYData = PLYData (ByteString, Header)
-
-instance Show PLYData where
-  show (PLYData (_,h)) = "PLYData <bytes> " ++ show h
-
-parseASCII :: Element -> Parser (Vector (Vector Scalar))
-parseASCII e = replicateM' (elNum e)
-                           (skip *> (V.fromList <$> multiProps (elProps e)))
-
-parseASCIIv3 :: forall a. PLYType a => Element -> Parser (VS.Vector (V3 a))
-parseASCIIv3 (Element _ n ps@[_,_,_])
-  | all samePropType ps = replicateM' n (skip *> (V3 <$> p <*> p <*> p))
-  | otherwise = empty
-  where t = plyType (undefined::a)
-        p = unsafeUnwrap <$> (parseScalar t <* skipSpace)
-        samePropType (ScalarProperty t' _) = t == t'
-        samePropType (ListProperty _ _) = False
-parseASCIIv3 _ = empty
-
--- |If the PLY header is successfully parsed, the 'PLYData' value
--- returned may be used with 'loadElements' and 'loadElementsV3' to
--- extract data.
-loadPLY :: ByteString -> Either String PLYData
-loadPLY = aux . parse header
-  where aux (Fail _t ctxt msg) = Left $ "Parse failed: "++msg++" in "++show ctxt
-        aux (Partial _) = Left "Incomplete header"
-        aux (Done t r) = Right $ PLYData (t, r)
-
--- |Load a PLY header from a file.
-loadHeader :: FilePath -> IO (Either String PLYData)
-loadHeader = fmap loadPLY . BS.readFile
-
--- |@loadElements elementName ply@ loads a 'Vector' of each vertex of
--- the requested element array. If you are extracting 3D data,
--- consider using 'loadElementsV3'.
-loadElements :: ByteString -> PLYData -> 
-                Either String (Vector (Vector Scalar))
-loadElements n (PLYData (body, (ASCII, ess))) = go ess body
-  where go [] _ = Left "Unknown element"
-        go (e:es) b | elName e == n = parseOnly (parseASCII e) b
-                    | otherwise = go es $
-                                  parseSkip (count (elNum e) line *> pure ()) b
-loadElements _ _ = error "Binary PLY is unsupported"
-{-# INLINABLE loadElements #-}
-
--- |Like 'loadElements', but restricted to 3D vectors. When it can be
--- used, this function is much more efficient than 'loadElements'.
-loadElementsV3 :: PLYType a => ByteString -> PLYData -> 
-                  Either String (VS.Vector (V3 a))
-loadElementsV3 n (PLYData (body, (ASCII, ess))) = go ess body
-  where go [] _ = Left "Unknown element"
-        go (e:es) b | elName e == n = parseOnly (parseASCIIv3 e) b
-                    | otherwise = go es $
-                                  parseSkip (count (elNum e) line *> pure ()) b
-loadElementsV3 _ _ = error "Binary PLY is unsupported"
-{-# INLINABLE loadElementsV3 #-}
-
-(>=!>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
-f >=!> g !x = f x >>= (g $!)
-infixr 1 >=!>
-
--- |Load all meshes identified by a @.conf@ file in parallel, and
--- transform vertex data into the coordinate frame specified by the
--- @.conf@ file. The application @loadMeshesV3 confFile element@ loads
--- @confFile@ to identify every @ply@ mesh to load. The @ply@ files
--- are loaded from the same directory that contained the @.conf@ file,
--- and the data associated with @element@ (e.g. @\"vertex\"@) is
--- loaded, transformed, and concatenated from all the meshes.
-loadMeshesV3 :: forall a. (PLYType a, Fractional a, Conjugate a, RealFloat a) =>
-                FilePath -> ByteString -> IO (Either [String] (VS.Vector (V3 a)))
-loadMeshesV3 confFile element = do dir <- takeDirectory <$> 
-                                          canonicalizePath confFile
-                                   c <- parseConf <$> BS.readFile confFile
-                                   let cam = let Right (Conf (t,r) _) = c
-                                             in fmap (fmap realToFrac) $
-                                                mkTransformation r t
-                                   either (return . Left . (:[]))
-                                          (fmap checkConcat . loadAllMeshes dir cam)
-                                          c
-    where checkErrors :: [Either String (VS.Vector (V3 a))]
-                      -> Either [String] [VS.Vector (V3 a)]
-          checkErrors xs = let (ls,rs) = partitionEithers xs
-                           in if null ls then Right rs else Left ls
-          checkConcat :: [Either String (VS.Vector (V3 a))]
-                      -> Either [String] (VS.Vector (V3 a))
-          checkConcat = (fmap VS.concat $!) . checkErrors
-          loadMesh :: FilePath -> M44 a -> (ByteString, Transformation Double)
-                   -> IO (Either String (VS.Vector (V3 a)))
-          loadMesh d _cam (f, (t,r)) = 
-            -- It is convenient to ignore the camera transformation so
-            -- that the object is at the origin.
-            let m = (^+^ fmap realToFrac t ) . rotate (conjugate (fmap realToFrac r))
-            in (loadPLY 
-                >=!> loadElementsV3 element
-                >=!> return . VS.map m)
-               <$> BS.readFile (d </> BC.unpack f)
-          loadAllMeshes :: FilePath -> M44 a -> Conf -> 
-                           IO ([Either String (VS.Vector (V3 a))])
-          loadAllMeshes dir cam = parallel . map (loadMesh dir cam) . meshes
-{-# INLINABLE loadMeshesV3 #-}
diff --git a/src/executable/Main.hs b/src/executable/Main.hs
--- a/src/executable/Main.hs
+++ b/src/executable/Main.hs
@@ -5,7 +5,7 @@
 import Control.Monad (when)
 import qualified Data.Vector.Storable as VS
 import Linear.V3
-import PLY.Data
+import PLY
 import System.Environment (getArgs)
 import System.IO (withBinaryFile, IOMode(WriteMode), hPutBuf)
 
@@ -13,8 +13,8 @@
 main = do args@(~[confFile, outputFile]) <- getArgs
           when (length args /= 2)
                (error "Usage: ply2bin confFile outputFile")
-          Right pts <- loadMeshesV3 confFile "vertex" 
-                         :: IO (Either [String] (VS.Vector (V3 Float)))
+          Right pts <- loadConfV3 "vertex" confFile
+                         :: IO (Either String (VS.Vector (V3 Float)))
           putStrLn $ "Loaded "++show (VS.length pts)++" vertices"
           withBinaryFile outputFile WriteMode $ \h ->
             VS.unsafeWith pts $ \ptr ->
