diff --git a/Data/Random/Choose.hs b/Data/Random/Choose.hs
new file mode 100644
--- /dev/null
+++ b/Data/Random/Choose.hs
@@ -0,0 +1,36 @@
+{- |
+
+"Data.Random.Choose" provides an efficient mechanism to select /n/ items
+uniformly at random from an input stream, for some fixed /n/.
+
+* "Data.Random.Choose.Tree" contains the implementation details.
+* "Data.Random.Choose.IO" puts everything together as a single function
+  (using IO).
+
+-}
+
+module Data.Random.Choose
+    ( -- * Algorithm outline
+      -- $outline
+    ) where
+
+import Data.Random.Choose.IO
+import Data.Random.Choose.Tree
+
+{- $outline
+
+We store items on a binary tree ('Tree'), moving them down the left or right
+branch according to a coin flip. At the end, the rightmost /n/ items on the tree
+are selected. Each time we 'insert' into a tree having /n/ items, we prune its
+leftmost item ('applyLimit' and 'evict').
+
+It may be helpful to think of this as lazily assigning each item a /d/-bit
+score, where /d/ is the maximum depth of the tree. Moving an item to the left
+or right corresponds to appending a 0 or 1 to its score. Note that the laziness
+is necessary, because /d/ is not known a priori.
+
+The process of moving items futher down the tree is referred to as
+disambiguation ('disambiguate') because its purpose is to resolve ties in the
+score.
+
+-}
diff --git a/Data/Random/Choose/IO.hs b/Data/Random/Choose/IO.hs
new file mode 100644
--- /dev/null
+++ b/Data/Random/Choose/IO.hs
@@ -0,0 +1,59 @@
+{- |
+
+'IO'-specific functionality around the random selection algorithm described in
+"Data.Random.Choose".
+
+-}
+
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Data.Random.Choose.IO ( getSelectionsIO ) where
+
+--------------------------------------------------------------------------------
+
+import qualified Data.Random.Choose.Tree as Tree
+
+import Data.Random.Choose.Tree (Tree)
+
+import Control.Monad.Random   (evalRandIO)
+
+import qualified Data.Foldable as Foldable
+
+import Data.Function ((&))
+import Data.List     (sort)
+
+--------------------------------------------------------------------------------
+
+getSelectionsIO :: forall a. (Ord a)
+    => IO (Maybe a) -- ^ Producer of items to choose from; produces 'Nothing'
+                    --   when there are no more items)
+    -> Int          -- ^ /n/: Number of items to choose
+    -> IO [a]       -- ^ /n/ of the items (or all of the items if fewer than
+                    --   /n/ are produced)
+
+-- ^ Selects /n/ items uniformly at random from an input stream.
+
+--------------------------------------------------------------------------------
+
+getSelectionsIO getItem limit = f 0 Tree.empty
+  where
+
+    -- We store each line of text along with its index (i) so that when we're
+    -- done, we can sort by the index, thus outputting the selected items in
+    -- the same order in which they were read.
+    f :: Int -> Tree (Int, a) -> IO [a]
+    f i tree = do
+        lineMaybe <- getItem
+        case lineMaybe of
+
+            -- We read a line; insert it into the tree and recurse.
+            Just line -> do
+                tree' <- evalRandIO ( tree
+                                    & Tree.insert (i, line)
+                                    & Tree.applyLimit limit
+                                    )
+                f (i + 1) tree'
+
+            -- We've reached the end of the input. Convert the tree to a list,
+            -- sort it, and strip out the indices to return just the text.
+            Nothing -> return $ snd <$> sort (Foldable.toList tree)
diff --git a/Data/Random/Choose/Tree.hs b/Data/Random/Choose/Tree.hs
new file mode 100644
--- /dev/null
+++ b/Data/Random/Choose/Tree.hs
@@ -0,0 +1,115 @@
+{- |
+
+Defines the 'Tree' data structure and operations on it to implement the random
+selection algorithm described in "Data.Random.Choose".
+
+-}
+
+module Data.Random.Choose.Tree
+    ( Tree(..), empty, insert, applyLimit, evict, disambiguate ) where
+
+--------------------------------------------------------------------------------
+
+import Control.Monad.Random (Rand, RandomGen, getRandom)
+
+--------------------------------------------------------------------------------
+
+data Tree a = Nil | Tree
+    { treeSize   :: Int    -- ^ Total number of items at this node and below
+    , treeValues :: [a]    -- ^ Items at this node
+    , treeLeft   :: Tree a -- ^ Left subtree (less likely for inclusion)
+    , treeRight  :: Tree a -- ^ Right subtree (more likely for inclusion)
+    }
+-- ^ A binary tree with arbitrarily many values at each node.
+
+instance Foldable Tree where
+
+    foldr _ z Nil = z
+    foldr f z (Tree size (x:xs) left right) =
+        foldr f (f x z) (Tree (size - 1) xs left right)
+    foldr f z (Tree size [] left right) =
+        (\z -> foldr f z left) . (\z -> foldr f z right) $ z
+
+    length Nil      = 0
+    length t@Tree{} = treeSize t
+
+    null Nil      = True
+    null t@Tree{} = treeSize t == 0
+
+empty :: Tree a
+-- ^ A tree with no elements.
+
+insert :: a -> Tree a -> Tree a
+-- ^ Trivial insertion into the root of a tree, increasing its size by 1
+--   and leaving its children unmodified.
+
+applyLimit :: (RandomGen g)
+    => Int -- ^ @limit@
+    -> Tree a
+    -> Rand g (Tree a)
+-- ^ Remove items from the tree until its size is at most @limit@.
+--   This may involve disambiguation if eviction takes place.
+
+evict :: (RandomGen g) => Tree a -> Rand g (Tree a)
+-- ^ Remove one item from the tree (or leave the tree unmodified if it is
+--   already empty). This may involve disambiguation if there is not already
+--   a clear leftmost item.
+
+disambiguate :: (RandomGen g) => Tree a -> Rand g (Tree a)
+-- ^ Perform disambiguation at the root level only, pushing items from
+--   the root down into subtrees as necessary.
+
+--------------------------------------------------------------------------------
+
+empty = Nil
+
+insert x Nil = Tree 1 [x] Nil Nil
+
+insert x (Tree size xs left right) = Tree (size + 1) (x:xs) left right
+
+applyLimit limit _ | limit <= 0 = pure Nil
+
+applyLimit limit tree
+
+    -- If the tree is small enough: We don't need to do anything.
+    | length tree <= limit = pure tree
+
+    -- If the tree is oversized: Remove an item from it, and recurse.
+    | otherwise = applyLimit limit =<< evict tree
+
+evict tree | length tree <= 1 = pure Nil
+
+evict tree = do
+    (Tree _ _ left right) <- disambiguate tree
+
+    -- Evict from one of the subtrees, preferring to evict from the left.
+    (left', right') <- if not . null $ left
+                       then (\x -> (x, right)) <$> evict left
+                       else (\x -> (left, x))  <$> evict right
+
+    return $ Tree (length left' + length right') [] left' right'
+
+-- For a tree with no items at the root, no disambiguation is possible
+-- (remember that disambiguate operates at the root only).
+disambiguate tree@(Tree _ [] _ _) = pure tree
+
+-- For a tree which contains a single item and no children, no
+-- disambiguation is required.
+disambiguate tree@(Tree _ [_] Nil Nil) = pure tree
+
+-- There is at least one item at the root that needs to be pushed down, to
+-- disambiguate it (either from items in subtrees, or from other items at
+-- the root).
+disambiguate (Tree size (x:xs) left right) = do
+
+    -- Randomly decide whether to push x into the left or right subtree.
+    b <- getRandom
+    let (left', right') = if b
+                          then (insert x left, right)
+                          else (left, insert x right)
+
+    -- In tree', a single item from the original tree has been pushed down.
+    let tree' = Tree size xs left' right'
+
+    -- There still may be other items at the root, so recurse.
+    disambiguate tree'
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/choose.cabal b/choose.cabal
new file mode 100644
--- /dev/null
+++ b/choose.cabal
@@ -0,0 +1,30 @@
+-- This file has been generated from package.yaml by hpack version 0.14.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:           choose
+version:        0.1.0.0
+synopsis:       Choose random elements from a stream.
+description:    Provides an efficient mechanism to select /n/ items uniformly at random from an input stream, for some fixed /n/.
+category:       Random
+author:         Chris Martin <ch.martin@gmail.com>
+maintainer:     Chris Martin <ch.martin@gmail.com>
+license:        Apache-2.0
+license-file:   license.txt
+build-type:     Simple
+cabal-version:  >= 1.10
+
+library
+  hs-source-dirs:
+      .
+  ghc-options: -Wall -fwarn-unused-imports
+  build-depends:
+      base >= 4.7 && < 5
+    , MonadRandom
+  exposed-modules:
+      Data.Random.Choose
+      Data.Random.Choose.Tree
+      Data.Random.Choose.IO
+  other-modules:
+      Paths_choose
+  default-language: Haskell2010
diff --git a/license.txt b/license.txt
new file mode 100644
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,13 @@
+Copyright 2016 Chris Martin
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
