diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
 # Revision history for selda
 
+## 0.1.2.0 -- 2017-04-20
+
+* Replace `¤` with `:*:` in table definitions.
+
+
 ## 0.1.1.1 -- 2017-04-20
 
 * Generic tables, queries and mutation.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -56,17 +56,17 @@
 `OverloadedStrings` extensions.
 
 Table schemas are defined as the product of one or more columns, stitched
-together using the `¤` operator.
+together using the `:*:` operator.
 A table is parameterized over the types of its columns, with the column types
-separated by the `:*:` operator. This, by the way, is why you need
+also separated by the `:*:` operator. This, by the way, is why you need
 `TypeOperators`.
 
 ```
 people :: Table (Text :*: Int :*: Maybe Text)
-people = table "people" $ primary "name" ¤ required "age" ¤ optional "pet"
+people = table "people" $ primary "name" :*: required "age" :*: optional "pet"
 
 addresses :: Table (Text :*: Text)
-addresses = table "addresses" $ required "name" ¤ required "city"
+addresses = table "addresses" $ required "name" :*: required "city"
 ```
 
 Columns may be either `required` or `optional`.
@@ -181,10 +181,10 @@
 ```
 people' :: Table (Int :*: Text :*: Int :*: Maybe Text)
 people' = table "people_with_ids"
-        $ autoPrimary "id"
-        ¤ required "name"
-        ¤ required "age"
-        ¤ optional "pet"
+  $   autoPrimary "id"
+  :*: required "name"
+  :*: required "age"
+  :*: optional "pet"
 
 populate' :: SeldaT IO ()
 populate' = do
diff --git a/selda.cabal b/selda.cabal
--- a/selda.cabal
+++ b/selda.cabal
@@ -1,5 +1,5 @@
 name:                selda
-version:             0.1.1.1
+version:             0.1.2.0
 synopsis:            Type-safe, high-level EDSL for interacting with relational databases.
 description:         This package provides an EDSL for writing portable, type-safe, high-level
                      database code. Its feature set includes querying and modifying databases,
diff --git a/src/Database/Selda.hs b/src/Database/Selda.hs
--- a/src/Database/Selda.hs
+++ b/src/Database/Selda.hs
@@ -34,12 +34,11 @@
   , update, update_
   , deleteFrom, deleteFrom_
     -- * Defining schemas
-  , ColSpec, TableName, ColName
+  , TableSpec, ColSpecs, ColSpec, TableName, ColName
   , NonNull, IsNullable, Nullable, NotNullable
-  , table, (¤), required, optional
+  , Append (..), (:++:)
+  , table, required, optional
   , primary, autoPrimary
-    -- * Combining schemas
-  , ComposeSpec, (:+++:), (+++)
     -- * Creating and dropping tables
   , createTable, tryCreateTable
   , dropTable, tryDropTable
@@ -82,6 +81,7 @@
 (.>=) = liftC2 $ BinOp Gte
 (.<=) = liftC2 $ BinOp Lte
 infixl 4 .==
+infixl 4 ./=
 infixl 4 .>
 infixl 4 .<
 infixl 4 .>=
diff --git a/src/Database/Selda/Caching.hs b/src/Database/Selda/Caching.hs
--- a/src/Database/Selda/Caching.hs
+++ b/src/Database/Selda/Caching.hs
@@ -139,13 +139,9 @@
 
 -- | Set the maximum number of items allowed in the cache.
 setMaxItems :: Int -> IO ()
-setMaxItems n = atomicModifyIORef' theCache $ \c -> (setMaxItems' n c, ())
-
--- | The the maximum number of items for the cache.
-getMaxItems :: IO Int
-getMaxItems = maxItems <$> readIORef theCache
+setMaxItems n = atomicModifyIORef' theCache $ \_ -> (setMaxItems' n, ())
 
-setMaxItems' :: Int -> ResultCache -> ResultCache
-setMaxItems' 0 _  = emptyCache
-setMaxItems' n rc = emptyCache {maxItems = n}
+setMaxItems' :: Int -> ResultCache
+setMaxItems' 0 = emptyCache
+setMaxItems' n = emptyCache {maxItems = n}
 #endif
diff --git a/src/Database/Selda/Generic.hs b/src/Database/Selda/Generic.hs
--- a/src/Database/Selda/Generic.hs
+++ b/src/Database/Selda/Generic.hs
@@ -247,21 +247,6 @@
   isMaybeType _ = True
 instance {-# OVERLAPPABLE #-} Traits a
 
--- | Normalized append of two inductive tuples.
---   Note that this will flatten any nested inductive tuples.
-type family a :++: b where
-  (a :*: b) :++: c = a :*: (b :++: c)
-  a         :++: b = a :*: b
-
-class Append a b where
-  app :: a -> b -> a :++: b
-
-instance {-# OVERLAPPING #-} Append b c => Append (a :*: b) c where
-  app (a :*: b) c = a :*: app b c
-
-instance ((a :*: b) ~ (a :++: b)) => Append a b where
-  app a b = a :*: b
-
 -- | The relation corresponding to the given type.
 type family Rel (rep :: * -> *) where
   Rel (M1 t c a)  = Rel a
diff --git a/src/Database/Selda/Table.hs b/src/Database/Selda/Table.hs
--- a/src/Database/Selda/Table.hs
+++ b/src/Database/Selda/Table.hs
@@ -1,5 +1,4 @@
-{-# LANGUAGE GADTs, TypeOperators, OverloadedStrings #-}
-{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, RankNTypes #-}
+{-# LANGUAGE TypeOperators, TypeFamilies, OverloadedStrings #-}
 {-# LANGUAGE UndecidableInstances, FlexibleInstances, ScopedTypeVariables #-}
 -- | Selda table definition language.
 module Database.Selda.Table where
@@ -10,38 +9,6 @@
 import Data.List (sort, group)
 import Data.Monoid
 
-type family a :+++: b where
-  (a :*: b) :+++: c = a :*: (b :+++: c)
-  a :+++: b         = a :*: b
-infixr 5 :+++:
-infixr 5 +++
-
-class ComposeSpec t a b where
-  -- | Combine the given tables or column specifications into a new
-  --   column specification which can be used to create a new table.
-  --   Useful for building composable table specifications.
-  --
-  --   Note that this function is only suitable for combining specifications
-  --   which have a concrete type. To build a column specification from scratch,
-  --   use '(¤)' instead.
-  (+++) :: t a -> t b -> ColSpec (a :+++: b)
-
-instance (ComposeSpec Table a b, ComposeSpec Table b c) =>
-         ComposeSpec Table (a :*: b) c where
-  a +++ b = ColSpec $ tableCols a ++ tableCols b
-
-instance {-# OVERLAPPABLE #-} ((a :+++: b) ~ (a :*: b)) =>
-         ComposeSpec Table a b where
-  a +++ b = ColSpec $ tableCols a ++ tableCols b
-
-instance (ComposeSpec ColSpec a b, ComposeSpec ColSpec b c) =>
-         ComposeSpec ColSpec (a :*: b) c where
-  ColSpec a +++ ColSpec b = ColSpec $ a ++ b
-
-instance {-# OVERLAPPABLE #-} ((a :+++: b) ~ (a :*: b)) =>
-         ComposeSpec ColSpec a b where
-  ColSpec a +++ ColSpec b = ColSpec $ a ++ b
-
 -- | A database table.
 --   Tables are parameterized over their column types. For instance, a table
 --   containing one string and one integer, in that order, would have the type
@@ -70,19 +37,7 @@
   }]
 
 -- | A table column specification.
-newtype ColSpec a = ColSpec [ColInfo]
-
--- | Combine two column specifications.
---   Table descriptions are built by chaining columns using this operator:
---
--- > people :: Table (Text :*: Int :*: Maybe Text)
--- > people = table "people" $ required "name" ¤ required "age" ¤ optional "pet"
---
---   To combine two pre-built tables into a table comprised of both tables'
---   fields, see '(+++)'.
-(¤) :: ColSpec a -> ColSpec b -> ColSpec (a :*: b)
-ColSpec a ¤ ColSpec b = ColSpec (a ++ b)
-infixr 1 ¤
+newtype ColSpec a = ColSpec {unCS :: [ColInfo]}
 
 -- | Used by 'IsNullable' to indicate a nullable type.
 data Nullable
@@ -131,11 +86,24 @@
 addAttr attr (ColSpec [ci]) = ColSpec [ci {colAttrs = attr : colAttrs ci}]
 addAttr _ _                 = error "impossible: SqlType ColSpec with several columns"
 
+-- | An inductive tuple where each element is a column specification.
+type family ColSpecs a where
+  ColSpecs (a :*: b) = ColSpec a :*: ColSpecs b
+  ColSpecs a         = ColSpec a
+
+-- | An inductive tuple forming a table specification.
+class TableSpec a where
+  mergeSpecs :: Proxy a -> ColSpecs a -> [ColInfo]
+instance TableSpec b => TableSpec (a :*: b) where
+  mergeSpecs _ (ColSpec a :*: b) = a ++ mergeSpecs (Proxy :: Proxy b) b
+instance {-# OVERLAPPABLE #-} ColSpecs a ~ ColSpec a => TableSpec a where
+  mergeSpecs _ (ColSpec a) = a
+
 -- | A table with the given name and columns.
-table :: TableName -> ColSpec a -> Table a
-table name (ColSpec cs) = Table
+table :: forall a. TableSpec a => TableName -> ColSpecs a -> Table a
+table name cs = Table
   { tableName = name
-  , tableCols = validate name $ map tidy cs
+  , tableCols = validate name $ map tidy $ mergeSpecs (Proxy :: Proxy a) cs
   }
 
 -- | Remove duplicate attributes.
diff --git a/src/Database/Selda/Types.hs b/src/Database/Selda/Types.hs
--- a/src/Database/Selda/Types.hs
+++ b/src/Database/Selda/Types.hs
@@ -1,5 +1,5 @@
 {-# LANGUAGE GADTs, TypeOperators, TypeFamilies, FlexibleInstances #-}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses #-}
 -- | Basic Selda types.
 module Database.Selda.Types where
 import Data.Text (Text)
@@ -82,3 +82,18 @@
 tenth :: Tup j => (a :*: b :*: c :*: d :*: e :*: f :*: g :*: h :*: i :*: j)
       -> Head j
 tenth (_ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: _ :*: j) = tupHead j
+
+-- | Normalized append of two inductive tuples.
+--   Note that this will flatten any nested inductive tuples.
+type family a :++: b where
+  (a :*: b) :++: c = a :*: (b :++: c)
+  a         :++: b = a :*: b
+
+class Append a b where
+  app :: a -> b -> a :++: b
+
+instance {-# OVERLAPPING #-} Append b c => Append (a :*: b) c where
+  app (a :*: b) c = a :*: app b c
+
+instance ((a :*: b) ~ (a :++: b)) => Append a b where
+  app a b = a :*: b
