packages feed

detrospector (empty) → 0.1

raw patch · 10 files changed

+393/−0 lines, 10 filesdep +basedep +binarydep +bytestringsetup-changed

Dependencies added: base, binary, bytestring, cmdargs, containers, hashable, hashmap, mwc-random, text, zlib

Files

+ Detrospector/Main.hs view
@@ -0,0 +1,58 @@+{-# OPTIONS_GHC+    -fno-warn-missing-fields #-}+module Detrospector.Main(main) where++import Detrospector.Modes++import qualified Detrospector.Modes.Train  as M+import qualified Detrospector.Modes.Run    as M+import qualified Detrospector.Modes.Neolog as M++import qualified System.Console.CmdArgs as Arg+import           System.Console.CmdArgs((+=),Annotate((:=)))++modes :: Annotate Arg.Ann+modes  = Arg.modes_  [train,run,neolog]+      += Arg.program "detrospector"+      += Arg.summary "detrospector: Markov chain text generator"+      += Arg.help    "Build and run Markov chains for text generation"+  where++  train = Arg.record Train{}+    [ num := 4+          += Arg.help "Number of characters lookback"+    , out := error "Must specify output chain"+          += Arg.typFile+          += Arg.help "Write chain to this file" ]+    += Arg.help "Train a Markov chain from standard input"+  +  run = Arg.record Run{}+    [ chain := error "Must specify input chain"+            += Arg.argPos 0+            += Arg.typ "CHAIN_FILE" ]+    += Arg.help "Generate random text"+  +  neolog = Arg.record Neolog{}+    [ chain    := error "Must specify input chain"+               += Arg.argPos 0+               += Arg.typ "CHAIN_FILE"+    , wordFile := "/usr/share/dict/words" --FIXME: platform?+               += Arg.typFile+               += Arg.explicit += Arg.name "w" += Arg.name "words"+               += Arg.help "List of real words, to be ignored"+    , minLen   := 5+               += Arg.explicit += Arg.name "m" += Arg.name "min"+               += Arg.help "Minimum length of a word"+    , maxLen   := 20+               += Arg.explicit += Arg.name "n" += Arg.name "max"+               += Arg.help "Maximum length of a word" ]+    += Arg.help "Invent new words not found in a dictionary"++dispatch :: Mode -> IO ()+dispatch m = case m of+  Train {} -> M.train  m+  Run   {} -> M.run    m+  Neolog{} -> M.neolog m++main :: IO ()+main = Arg.cmdArgs_ modes >>= dispatch
+ Detrospector/Modes.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE+    DeriveDataTypeable #-}+module Detrospector.Modes(Mode(..), ModeFun) where++import Data.Typeable(Typeable)+import Data.Data    (Data    )++data Mode+  = Train   { num      :: Int+            , out      :: FilePath }++  | Run     { chain    :: FilePath }++  | Neolog  { chain    :: FilePath+            , minLen   :: Int+            , maxLen   :: Int+            , wordFile :: FilePath }++  deriving (Show, Typeable, Data)++type ModeFun = Mode -> IO ()
+ Detrospector/Modes/Neolog.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE+    NamedFieldPuns+  , BangPatterns #-}+module Detrospector.Modes.Neolog(neolog) where++import Detrospector.Types+import Detrospector.Modes++import Data.Char+import Control.Applicative+import qualified Data.HashSet      as HS+import qualified Data.Text.Lazy    as Txt+import qualified Data.Text.Lazy.IO as Txt++-- Generate neologisms.+go :: Mode -> Chain -> RNG -> IO ()+go Neolog{minLen,maxLen,wordFile} c@(Chain n _) g+  = getWords >>= loop where++  getWords = (HS.fromList . map (listToQueue . Txt.unpack) . Txt.lines)+    <$> Txt.readFile wordFile++  loop !wd = do+    xs  <- fmap toLower <$> make emptyQ emptyQ+    wdd <- if (qLength xs >= minLen) && HS.notMember xs wd+      then putStrLn (queueToList xs) >> return (HS.insert xs wd)+      else return wd+    loop wdd++  make !xs !s | qLength xs >= maxLen = return xs+              | otherwise = do+      (x,h) <- pick c s g+      -- give up if we don't use all history+      if (h == qLength s) && isAlpha x+        then make (xs `qSnoc` x) (shift n x s)+        else return xs++go _ _ _ = error "impossible: wrong mode passed to neolog"++neolog :: ModeFun+neolog m@Neolog{chain} = withChain chain $ go m+neolog _ = error "impossible: wrong mode passed to neolog"
+ Detrospector/Modes/Run.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE+    NamedFieldPuns #-}+module Detrospector.Modes.Run(run) where++import Detrospector.Types+import Detrospector.Modes++go :: Chain -> RNG -> IO ()+go c@(Chain n _) g = loop emptyQ where+    loop s = do+      (x,_) <- pick c s g+      putChar x+      loop $! shift n x s++run :: ModeFun+run Run{chain} = withChain chain go+run _ = error "impossible: wrong mode passed to run"
+ Detrospector/Modes/Train.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE+    NamedFieldPuns+  , BangPatterns  #-}+module Detrospector.Modes.Train(train) where++import Detrospector.Types+import Detrospector.Modes++import System.IO+import qualified Data.Text         as TS+import qualified Data.Text.Lazy    as TL+import qualified Data.Text.Lazy.IO as TL+import qualified Data.HashMap      as H+import qualified Data.IntMap       as IM+import qualified Data.Sequence     as S+import qualified Data.Foldable     as F++-- foldl' with progress dots+progFold :: (a -> b -> a) -> a -> [b] -> IO a+progFold f = go where+  go !v []     = return v+  go !v (x:xs) = putChar '.' >> go (f v x) xs++-- Build a Markov chain with n-Char history from some input text.+train :: ModeFun+train Train{num,out} = do+  hSetBuffering stdout NoBuffering+  ys <- TL.getContents+  putStr "Calculating"+  (_,h) <- progFold (TS.foldl' roll) (emptyQ,H.empty) $ TL.toChunks ys+  putStrLn "done."+  writeChain out . Chain num $ H.map cumulate h where++  roll (!s,!h) x+    = (shift num x s, F.foldr (H.alter $ ins x) h $ S.tails s)++  ins x Nothing  = Just $! sing x+  ins x (Just v) = Just $! incr x v++  sing x = IM.singleton (fromEnum x) 1++  incr x = IM.alter f $ fromEnum x where+    f Nothing  = Just 1+    f (Just v) = Just $! (v+1)++train _ = error "impossible: wrong mode passed to train"
+ Detrospector/Types.hs view
@@ -0,0 +1,126 @@+{-# LANGUAGE+    ViewPatterns+  , PatternGuards #-}+{-# OPTIONS_GHC+  -fno-warn-orphans #-}+module Detrospector.Types(+    FreqTable+  , PickTable+  , cumulate+  , RNG+  , Queue, emptyQ, listToQueue, queueToList, qLength, qSnoc+  , shift+  , Chain(..), pick+  , withChain, writeChain) where++import Data.Maybe+import Data.List+import Control.Applicative+import qualified Data.HashMap           as H+import qualified Data.Hashable          as H+import qualified Data.IntMap            as IM+import qualified System.Random.MWC      as RNG+import qualified Data.Sequence          as S+import qualified Data.Foldable          as F+import qualified Data.Binary            as Bin+import qualified Data.ByteString.Lazy   as BSL+import qualified Codec.Compression.GZip as Z++-- This table records character frequency;+-- each Int key is the codepoint of a Char.+type FreqTable = IM.IntMap Int++-- A table for efficiently sampling a finite+-- discrete distribution of Char.+--+-- To sample from (PickTable n im):+-- * Pick random k from [0,n) uniformly+-- * Take value of first key > k+data PickTable = PickTable Int (IM.IntMap Char)+  deriving (Show)++cumulate :: FreqTable -> PickTable+cumulate t = PickTable r $ IM.fromList ps where+  (r,ps) = mapAccumR f 0 $ IM.assocs t+  f ra (x,n) = let rb = ra+n in (rb, (rb, toEnum x))++type RNG = RNG.GenIO++-- Sample from a PickTable.+sample :: PickTable -> RNG -> IO Char+sample (PickTable t im) g = do+  k <- (`mod` t) <$> RNG.uniform g+  case IM.split k im of+    -- last key in im is t, and we know k < t+    -- therefore the right list cannot be empty+    (_, IM.toList -> ((_,x):_)) -> return x+    _ -> error "impossible"++type Queue a = S.Seq a++emptyQ :: Queue a+emptyQ = S.empty++listToQueue :: [a] -> Queue a+listToQueue = S.fromList++queueToList :: Queue a -> [a]+queueToList = F.toList++qLength :: Queue a -> Int+qLength = S.length++qSnoc :: Queue a -> a -> Queue a+qSnoc = (S.|>)++-- Enqueue at one side while dropping from the other+shift :: Int -> a -> Queue a -> Queue a+shift n x q+  | S.length q < n          = q S.|> x+  | (_ S.:< s) <- S.viewl q = s S.|> x+  | otherwise               = q S.|> x++-- The Markov chain itself.+-- (Chain n hm) maps subsequences of up to n Chars to finite+-- Char distributions represented by PickTables in hm.+data Chain = Chain Int (H.HashMap (Queue Char) PickTable)+  deriving (Show)++-- Pick from a chain according to history.  Returns a character+-- and the amount of history actually used.+pick :: Chain -> Queue Char -> RNG -> IO (Char, Int)+pick (Chain _ h) s g = do x <- sample pt g; return (x,hn) where+  (pt, hn) = get s++  -- assumption: map is nonempty for empty key+  get  t = fromMaybe (get $ qTail t) look where+    qTail (S.viewl -> (_ S.:< r)) = r+    qTail _ = error "qTail: empty queue"+    look = do x <- H.lookup t h; return (x, S.length t)++-- orphan instance: make Seq hashable+instance (H.Hashable a) => H.Hashable (S.Seq a) where+  {-# SPECIALIZE instance H.Hashable (S.Seq Char) #-}+  hash = F.foldl' (\acc h -> acc `H.combine` H.hash h) 0++-- orphan instance: Binary serialization of HashMap+instance (Bin.Binary k, Bin.Binary v, H.Hashable k, Ord k)+       => Bin.Binary (H.HashMap k v) where+  put = Bin.put . H.assocs+  get = H.fromList <$> Bin.get+  +instance Bin.Binary PickTable where+  put (PickTable n t) = Bin.put (n,t)+  get = uncurry PickTable <$> Bin.get++instance Bin.Binary Chain where+  put (Chain n h) = Bin.put (n,h)+  get = uncurry Chain <$> Bin.get++withChain :: FilePath -> (Chain -> RNG -> IO a) -> IO a+withChain p f = do+  ch <- (Bin.decode . Z.decompress) <$> BSL.readFile p+  RNG.withSystemRandom $ f ch++writeChain :: FilePath -> Chain -> IO ()+writeChain out = BSL.writeFile out . Z.compress . Bin.encode
+ LICENSE view
@@ -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.
+ Setup.hs view
@@ -0,0 +1,4 @@+#! /usr/bin/runhaskell++import Distribution.Simple+main = defaultMain
+ detrospector.cabal view
@@ -0,0 +1,47 @@+name:                detrospector+version:             0.1+license:             BSD3+license-file:        LICENSE+synopsis:            Markov chain text generator+category:            Text, Natural Language Processing+author:              Keegan McAllister <mcallister.keegan@gmail.com>+maintainer:          Keegan McAllister <mcallister.keegan@gmail.com>+build-type:          Simple+cabal-version:       >=1.2+description:+  The `detrospector' program generates random text conforming to the general+  style and diction of a given source document.  It associates each+  `k'-character substring of the source document with a probability+  distribution for the next character.  These distributions are used to+  iteratively pick new characters for output.  In other words, it samples a+  Markov chain derived from the source document.+  .+  Run `detrospector' `-?' for usage information.  The program has several+  modes.  It can generate random text, or invent individual random words which+  are not found in a dictionary.  These modes require a chain file, which is+  built from a source document in another mode.+  .+  Design goals include speed and full Unicode support.  I welcome suggestions+  and patches regarding any aspect of this program.++executable detrospector+  main-is:       detrospector.hs+  other-modules:+    Detrospector.Main+    Detrospector.Types+    Detrospector.Modes+    Detrospector.Modes.Train+    Detrospector.Modes.Run+    Detrospector.Modes.Neolog+  build-depends:+    base       >= 3 && < 5,+    containers >= 0.3,+    text       >= 0.8,+    zlib       >= 0.5,+    bytestring >= 0.9,+    binary     >= 0.5,+    cmdargs    >= 0.6,+    mwc-random >= 0.8,+    hashable   >= 1.0,+    hashmap    >= 1.1+  ghc-options: -Wall
+ detrospector.hs view
@@ -0,0 +1,6 @@+module Main(main) where++import qualified Detrospector.Main as D++main :: IO ()+main = D.main