diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) Keegan McAllister 2010
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. 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.
+3. Neither the name of the author nor the names of his 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 AUTHORS 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,4 @@
+#! /usr/bin/runhaskell
+
+import Distribution.Simple
+main = defaultMain
diff --git a/Text/RowRecord.hs b/Text/RowRecord.hs
new file mode 100644
--- /dev/null
+++ b/Text/RowRecord.hs
@@ -0,0 +1,131 @@
+{-# LANGUAGE
+    ViewPatterns
+  , TypeSynonymInstances #-}
+
+-- | Convert lists of strings to records.
+
+module Text.RowRecord
+  ( -- * Tables
+    Column
+  , Row
+  , Table
+  , fromStrings
+
+    -- * Parsing fields
+  , Result  (..)
+  , RowError(..)
+  , Field   (..)
+  , require
+  , safeRead
+  , getField
+
+    -- * Parsing tables
+  , ParseRow(..)
+  , parseTable
+  ) where
+
+import Control.Applicative
+import Control.Monad
+
+import qualified Data.Map as M
+
+-- | Identifies a column.
+type Column = String
+
+-- | A row of @'String'@ data.
+type Row   = M.Map Column String
+
+-- | A table.
+type Table = [Row]
+
+-- | Convert a list of @'String'@ rows into a @'Table'@.
+-- Uses the first row as column names.
+fromStrings :: [[String]] -> Result Table
+fromStrings []     = Failure $ MissingField "header"
+fromStrings (h:xs) = Success $ map (M.fromList . zip h) xs
+
+-- | A parse result.
+data Result a
+  = Success a
+  | Failure RowError
+  deriving (Show)
+
+-- | Possible errors from parsing a row.
+data RowError
+  = MissingField Column
+  | NoParse      Column String
+  deriving (Show)
+
+instance Functor Result where
+  fmap f (Success v) = Success $ f v
+  fmap _ (Failure e) = Failure e
+
+instance Monad Result where
+   return = Success
+   Success x >>= f = f x
+   Failure e >>= _ = Failure e
+
+instance Applicative Result where
+  pure = return
+  (<*>) = ap
+
+-- | Class of field types which can be decoded from @'String'@.
+--
+-- The input can be @'Nothing'@ to represent a missing field.
+-- The instance @Field a => Field (Maybe a)@ models optional fields.
+--
+-- If your record contains custom types, you must create a @'Field'@
+-- instance for each.  If you have base types but need different 
+-- parsing behavior, you can use a @newtype@ wrapper.
+class Field a where
+  decode :: Maybe String -> Result a
+
+-- | @'read'@ in @'Maybe'@.
+safeRead :: (Read a) => String -> Maybe a
+safeRead (reads -> [(v,"")]) = Just v
+safeRead _ = Nothing
+
+-- | Implement @'decode'@ for a required field.
+require :: (String -> Maybe a) -> Maybe String -> Result a
+require _ Nothing = Failure $ MissingField ""
+require f (Just (f -> Just v)) = Success v
+require _ (Just xs) = Failure $ NoParse "" xs
+
+instance Field Bool    where decode = require safeRead
+instance Field Int     where decode = require safeRead
+instance Field Integer where decode = require safeRead
+instance Field Float   where decode = require safeRead
+instance Field Double  where decode = require safeRead
+instance Field String  where decode = require Just
+
+instance Field Char where
+  decode = require f where
+    f [x] = Just x
+    f _   = Nothing
+
+instance (Field a) => Field (Maybe a) where
+  decode Nothing       = Success Nothing
+  decode (Just "")     = Success Nothing
+  decode (decode -> r) = Just <$> r
+
+-- | Decode a field by column name.
+--
+-- Called from TH-generated code, but may be
+-- useful independently.
+getField :: (Field a) => Column -> Row -> Result a
+getField k m = case decode $ M.lookup k m of
+  v@(Success _)  -> v
+  Failure e -> Failure $ case e of
+    MissingField _ -> MissingField k
+    (NoParse _ s)  -> NoParse k s
+
+-- | Class of types which can be parsed from a @'Row'@.
+-- These types are typically single-constructor records.
+--
+-- Instances may be generated using @Text.RowRecord.TH@.
+class ParseRow a where
+  parseRow :: Row -> Result a
+
+-- | Parse a whole table.
+parseTable :: (ParseRow a) => Table -> Result [a]
+parseTable = mapM parseRow
diff --git a/Text/RowRecord/TH.hs b/Text/RowRecord/TH.hs
new file mode 100644
--- /dev/null
+++ b/Text/RowRecord/TH.hs
@@ -0,0 +1,68 @@
+{-# LANGUAGE
+    TemplateHaskell
+  , ViewPatterns
+  , FlexibleInstances #-}
+
+-- | Generate instances for converting lists of strings to records.
+
+module Text.RowRecord.TH
+  ( rowRecords
+  ) where
+
+import Text.RowRecord
+
+import Control.Applicative
+
+import Language.Haskell.TH
+import Language.Haskell.TH.Syntax
+
+-- Turn a record label name into a column name.
+-- Drop everything through the first '_'.
+getColumn :: Name -> Column
+getColumn = f . nameBase where
+  f (break (=='_') -> (_, ('_':xs))) = xs
+  f n = error ("RowRecord: bad label: " ++ n)
+
+-- | Generate a @'ParseRow'@ instance for each of the named types.
+--
+-- Each type must have exactly one constructor, in record style.
+--
+-- Column names are derived from the record field names by dropping the first
+-- @'_'@-separated component.  This allows for a prefix to disambiguate record
+-- labels between types.
+--
+-- For example, with
+--
+-- > data Foo = Foo
+-- >   { f_bar :: String
+-- >   , f_baz :: Int }
+-- > $(rowRecords [''Foo])
+--
+-- we can parse files of the form
+-- 
+-- > bar,baz
+-- > abc,3
+-- > def,5
+--
+-- assuming an appropriate CSV parser.
+
+rowRecords :: [Name] -> Q [Dec]
+rowRecords ns = concat <$> mapM rowRecord ns
+
+rowRecord :: Name -> Q [Dec]
+rowRecord n = do
+  i <- reify n
+  case i of
+    TyConI (DataD    _ _ _ [c] _) -> fromCon n c
+    TyConI (NewtypeD _ _ _ c   _) -> fromCon n c
+    _ -> error ("RowRecord: not a data type: " ++ show n)
+
+fromCon :: Name -> Con -> Q [Dec]
+fromCon ty (RecC ctor fields) =
+  let gets m = [ [| getField $(lift $ getColumn n) $(varE m) |]
+               | (n,_,_) <- fields ]
+      splat a b = [| $a <*> $b |]
+      bdy = [| \m -> $(foldl1 splat ([| pure $(conE ctor) |] : gets 'm)) |]
+  in  [d| instance ParseRow $(conT ty) where { parseRow = $bdy } |]
+
+fromCon _ c = error ("RowRecord: not a record: " ++ show c)
diff --git a/rowrecord.cabal b/rowrecord.cabal
new file mode 100644
--- /dev/null
+++ b/rowrecord.cabal
@@ -0,0 +1,35 @@
+name:                rowrecord
+version:             0.1
+license:             BSD3
+license-file:        LICENSE
+synopsis:            Build records from lists of strings, as from CSV files.
+category:            Data, Text
+author:              Keegan McAllister <mcallister.keegan@gmail.com>
+maintainer:          Keegan McAllister <mcallister.keegan@gmail.com>
+build-type:          Simple
+cabal-version:       >=1.2
+description:
+  Given rows of @String@ data with column headings, this library will create
+  values of user-defined record types.  Records can contain mandatory or
+  optional fields of any type, subject to a class constraint.
+  Heading names and and record construction code are derived using
+  Template Haskell.
+  .
+  One use case for this library is parsing records from a CSV file.  A
+  parser from CSV to @[[String]]@ is not included, but there are several
+  suitable packages on Hackage.
+  .
+  The emphasis of this library is on simplicity of use rather than performance.
+  It is likely to be suitable for a hundred thousand rows, but not many
+  millions.  A future version may support packed input formats like
+  @ByteString@ or @Text@.  Suggestions and patches are welcome.
+
+library
+  exposed-modules:
+      Text.RowRecord
+    , Text.RowRecord.TH
+  ghc-options:       -Wall
+  build-depends:
+      base             >= 3 && < 5
+    , template-haskell >= 2.4
+    , containers       >= 0.3
