packages feed

radixtree-0.6.0.0: src/Data/RadixTree.hs

{-# LANGUAGE BangPatterns          #-}
{-# LANGUAGE DeriveDataTypeable    #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE InstanceSigs          #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLists       #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE UndecidableInstances  #-}
module Data.RadixTree
  ( RadixTree (..)
  , RadixNode (..)
  , CompressedRadixTree
    -- * Construction
  , fromFoldable_
  , fromFoldable
  , compressBy
    -- * Parsing with radix trees
  , RadixParsing (..)
  , parse_
  , lookup_
  , search
  ) where
import           Control.Applicative
import           Control.DeepSeq
import           Data.Data               (Data, Typeable)
import           Data.Foldable           (asum, foldr', toList)
import           Data.Map.Strict         (Map)
import qualified Data.Map.Strict         as M
import           Data.Sequence           (Seq)
import qualified Data.Sequence           as Seq
import           Data.Text               (Text)
import qualified Data.Text               as T
import qualified Data.Text.Array         as TI (Array)
import qualified Data.Text.Internal      as TI (Text (..), text)
import           Data.Vector             (Vector)
import qualified Data.Vector             as V
import           Lens.Micro
import           Text.Parser.Char        (CharParsing (anyChar, text))
import           Text.Parser.Combinators (Parsing (try))

--------------------------------------------------------------------------------
-- Stuff to help construct RadixTrees
--
-- I'm not clever enough to write a function to go directly from a 'Foldable' to
-- a fully-optimised RadixTree. Instead, I generate a prefix-tree using a 'Map'
-- directly ('Trie'), and then gradually compress that ('CompressedTrie') before
-- packing the final result into an efficient structure using 'Text' nodes.
--
-- TODO:
-- - generate RadixTree directly, instead of going through 'Trie'/'CompressedTrie'
-- - use compact regions?

data PrefixNode a tree = Accept !Text a !tree | Skip !tree
  deriving (Show, Eq)

newtype Trie a = Trie (PrefixNode a (Map Char (Trie a)))
  deriving (Show, Eq)

newtype CompressedTrie a = CompressedTrie (PrefixNode a (Map (Seq Char) (CompressedTrie a)))
  deriving (Show, Eq)

{-# INLINE node #-}
node :: Lens (PrefixNode p a) (PrefixNode p b) a b
node = lens
  (\x -> case x of { Accept _ _ t -> t; Skip t -> t })
  (\x a -> case x of { Accept l p _ -> Accept l p a; Skip _ -> Skip a })

leaf :: Text -> Text -> a -> Trie a
leaf ft t v = go (T.unpack t)
  where
    go (x:xs) = Trie (Skip (M.singleton x (go xs)))
    go []     = Trie (Accept ft v M.empty)

insert :: Text -> Text -> a -> Trie a -> Trie a
insert ft text' a (Trie n) = case T.uncons text' of
  Just (c, cs) -> Trie ((node %~
    M.insertWith
    (\_ orig -> Data.RadixTree.insert ft cs a orig)
    c
    (leaf ft cs a)) n)
  Nothing ->
    Trie (n^.node.to (Accept ft a))

makeCompressable :: Trie a -> CompressedTrie a
makeCompressable (Trie n) = CompressedTrie (
  over node (M.map makeCompressable . M.mapKeysMonotonic Seq.singleton) n)

compress :: Trie a -> CompressedTrie a
compress = go . makeCompressable
  where
    go :: CompressedTrie a -> CompressedTrie a
    go (CompressedTrie n) = case n of
      Accept l p m -> CompressedTrie (Accept l p (M.map go m))
      Skip m       -> CompressedTrie (Skip (M.foldMapWithKey compress1 m))

    compress1 :: Seq Char -> CompressedTrie a -> Map (Seq Char) (CompressedTrie a)
    compress1 k c@(CompressedTrie n) =
      case M.size sm of
        0 -> M.singleton k c
        1 | Skip _ <- n -> compress1 (k <> k') sm'
          where (k', sm') = M.findMax sm
        _ -> M.singleton k (go (n & node .~ sm & CompressedTrie))
      where sm = n^.node

--------------------------------------------------------------------------------

-- | A node in a radixtree. To advance from here a parser must parse the 'Text'
-- (i.e., the prefix) value at this node.
data RadixNode a = RadixNode {-# UNPACK #-} !Text !(RadixTree a)
  deriving (Eq, Show, Typeable, Data)

-- | A radixtree. Construct with 'fromFoldable_, and use with 'parse'.
data RadixTree a
  = -- | Can terminate a parser successfully, returning the 'Text' value given.
    RadixAccept
    {-# UNPACK #-} !Text -- ^ text to return at this point
    {-# UNPACK #-} !(Vector (RadixNode a)) -- ^ possible subtrees beyond this point
    a -- ^ value to return at this point
  | RadixSkip
    {-# UNPACK #-} !(Vector (RadixNode a)) -- ^ possible subtrees beyond this point
  deriving (Eq, Show, Typeable, Data)

instance NFData a => NFData (RadixNode a) where
  {-# INLINE rnf #-}
  rnf (RadixNode l t) = rnf l `seq` rnf t

instance NFData a => NFData (RadixTree a) where
  {-# INLINE rnf #-}
  rnf (RadixAccept t v p) = t `seq` rnf p `seq` rnf v
  rnf (RadixSkip v)       = rnf v

-- | Compress a totally-unoptimised 'Trie' into a nice and easily-parsable
-- 'RadixTree'
fromTrie :: Trie a -> RadixTree a
fromTrie = go . compress
  where
    !z = V.empty

    radixNode :: Seq Char -> CompressedTrie a -> RadixNode a
    radixNode l t = RadixNode (T.pack (toList l)) (go t)

    mapToVector :: Map k a -> Vector (k, a)
    mapToVector m = case M.size m of
      0  -> z
      sz -> V.fromListN sz (M.toList m)

    go :: CompressedTrie a -> RadixTree a
    go (CompressedTrie n) = case n of
      Accept l p m -> RadixAccept l (V.map (uncurry radixNode) (mapToVector m)) p
      Skip m       -> RadixSkip . V.map (uncurry radixNode) . mapToVector $! m

data TextSlice = TextSlice
  { tsOffset16 :: {-# UNPACK #-} !Int -- ^ offset (in units of Word16)
  , tsLength16 :: {-# UNPACK #-} !Int -- ^ length (in units of Word16)
  }

-- | Probably dangerous magic
--
-- When the second argument is found to be within the first, we re-use the
-- 'Text' array of the first. This should allow the second argument to be
-- garbage collected. This is to improve locality and memory use.
magicallySaveSpaceSometimes :: Text -> Text -> Maybe TextSlice
magicallySaveSpaceSometimes full s@(TI.Text _ _ slen) =
  case T.breakOn s full of
    (TI.Text{}, r@(TI.Text _ remoffs _))
      | T.null r  -> Nothing
      | otherwise -> Just TextSlice{tsOffset16 = remoffs, tsLength16 = slen}

-- | A normal 'RadixTree' stores a new 'Text' at every node. In contrast, a
-- 'CompressedRadixTree' takes a single corpus 'Text' which is indexed into by
-- nodes. This can save a lot of memory (e.g., using the radix trees from the
-- parsing benchmarks in this package, the 'CompressedRadixTree' version is
-- 254032 bytes, whereas the ordinary 'RadixTree' is a rotund 709904 bytes) at
-- no runtime cost.
data CompressedRadixTree a
  = CompressedRadixTree {-# UNPACK #-} !TI.Array !(CompressedRadixTree1 a)

data CompressedRadixTree1 a
  = CompressedRadixAccept
    {-# UNPACK #-} !TextSlice
    {-# UNPACK #-} !(Vector (CompressedRadixNode a))
    a
  | CompressedRadixSkip {-# UNPACK #-} !(Vector (CompressedRadixNode a))

data CompressedRadixNode a
  = CompressedRadixNode {-# UNPACK #-} !TextSlice !(CompressedRadixTree1 a)

instance NFData a => NFData (CompressedRadixNode a) where
  {-# INLINE rnf #-}
  rnf (CompressedRadixNode ts t) = ts `seq` rnf t

instance NFData a => NFData (CompressedRadixTree a) where
  {-# INLINE rnf #-}
  rnf (CompressedRadixTree arr v) = arr `seq` rnf v

instance NFData a => NFData (CompressedRadixTree1 a) where
  {-# INLINE rnf #-}
  rnf (CompressedRadixAccept ts v a) = ts `seq` rnf a `seq` rnf v
  rnf (CompressedRadixSkip v)        = rnf v

-- | Compress a 'RadixTree' given a corpus. All values in the tree must be
-- findable within the corpus, though the corpus does not have to necessarily be
-- the direct source of the tree
compressBy :: Text -> RadixTree a -> Maybe (CompressedRadixTree a)
compressBy full@(TI.Text arr _ _) rt =
  CompressedRadixTree arr <$> recompressT rt

  where
    magic = magicallySaveSpaceSometimes full

    recompressN :: RadixNode a -> Maybe (CompressedRadixNode a)
    recompressN (RadixNode t tree) = CompressedRadixNode <$> magic t <*> recompressT tree

    recompressT :: RadixTree a -> Maybe (CompressedRadixTree1 a)
    recompressT (RadixSkip v)       = CompressedRadixSkip <$> V.mapM recompressN v
    recompressT (RadixAccept t v a) = CompressedRadixAccept <$> magic t <*> V.mapM recompressN v <*> pure a

-- | *Slow*. Same as 'fromFoldable', but you do not need to supply pairs of text
-- and values; they will default to '()'.
fromFoldable_ :: Foldable f => f Text -> RadixTree ()
fromFoldable_ =
  fromTrie . foldr' (\t -> insert t t ()) (Trie (Skip M.empty))

-- | *Slow*
fromFoldable :: Foldable f => f (Text, a) -> RadixTree a
fromFoldable =
  fromTrie . foldr' (\(t, a) -> insert t t a) (Trie (Skip M.empty))

--------------------------------------------------------------------------------
-- Parsers from 'RadixTree's

class RadixParsing radixtree where
  keys :: radixtree a -> [(Text, a)]
  parse :: CharParsing m => (Text -> a -> r) -> radixtree a -> m r
  lookup :: radixtree a -> Text -> Maybe (Text, a)

{-# INLINE search #-}
-- | Find all occurences of the terms in a 'RadixTree' from this point on. This
-- will consume the entire remaining input. Can lazily produce results (but this
-- depends on your parser).
search
  :: (Monad m, CharParsing m, RadixParsing radixtree)
  => radixtree a
  -> m [Text]
search r = go
  where
    go =
      (parse const r >>= \x -> (x:) <$> go) <|>
      (anyChar >> go) <|>
      return []

{-# INLINE parse_ #-}
parse_ :: (RadixParsing r, CharParsing m) => r a -> m Text
parse_ = Data.RadixTree.parse const

{-# INLINE lookup_ #-}
lookup_ :: RadixParsing r => r a -> Text -> Maybe Text
lookup_ r t = fst <$> Data.RadixTree.lookup r t

instance RadixParsing RadixTree where
  keys = go []
    where
      go nil (RadixAccept l xs a) = (l,a) : V.foldr (\(RadixNode _ x) xs' -> go xs' x) nil xs
      go nil (RadixSkip xs) = V.foldr (\(RadixNode _ x) xs' -> go xs' x) nil xs

  {-# INLINE parse #-}
  -- | Parse from a 'RadixTree'
  parse :: CharParsing m => (Text -> a -> r) -> RadixTree a -> m r
  parse constr = go
    where
      go r = case r of
        RadixAccept l nodes a
          | T.null l -> empty
          | otherwise -> asum (V.map parseRadixNode nodes) <|> pure (constr l a)
        RadixSkip nodes ->
          asum (V.map parseRadixNode nodes)

      {-# INLINE parseRadixNode #-}
      parseRadixNode (RadixNode prefix tree)
        | T.null prefix = go tree
        | otherwise     = try (text prefix *> go tree)

  lookup :: RadixTree a -> Text -> Maybe (Text, a)
  lookup rt0 t0
    | T.null t0 = case rt0 of
        RadixAccept v _ a -> Just (v, a)
        RadixSkip _       -> Nothing
    | otherwise = case rt0 of
        RadixAccept _ ns _ -> lookupRadixNodes t0 ns
        RadixSkip     ns   -> lookupRadixNodes t0 ns
    where
      lookupRadixNodes t v = go 0
        where
          !vlen = V.length v
          go !i
            | i < vlen  = case V.unsafeIndex v i of
                RadixNode pfix rt -> case T.commonPrefixes pfix t of
                  Just (_, remPfx, remSfx)
                    | T.null remPfx -> Data.RadixTree.lookup rt remSfx
                    | otherwise     -> Nothing
                  Nothing -> go (i + 1)
            | otherwise = Nothing

instance RadixParsing CompressedRadixTree where

  keys (CompressedRadixTree arr crt) = go [] crt
    where
      fromSlice (TextSlice offs len) = TI.text arr offs len

      go nil (CompressedRadixAccept l xs a) =
        (fromSlice l, a) : V.foldr (\(CompressedRadixNode _ x) xs' -> go xs' x) nil xs
      go nil (CompressedRadixSkip xs) =
        V.foldr (\(CompressedRadixNode _ x) xs' -> go xs' x) nil xs

  {-# INLINE parse #-}
  -- | Parse from a 'RadixTree'
  parse :: CharParsing m => (Text -> a -> r) -> CompressedRadixTree a -> m r
  parse constr (CompressedRadixTree arr crt) = go crt
    where
      fromSlice (TextSlice offs len) = TI.text arr offs len
      go r = case r of
        CompressedRadixAccept ts nodes a -> case fromSlice ts of
          l | T.null l -> empty
            | otherwise -> asum (V.map parseRadixNode nodes) <|> pure (constr l a)
        CompressedRadixSkip nodes -> asum (V.map parseRadixNode nodes)
      {-# INLINE parseRadixNode #-}
      parseRadixNode (CompressedRadixNode ts tree) = case fromSlice ts of
        prefix | T.null prefix -> go tree
               | otherwise     -> try (text prefix *> go tree)

  lookup :: CompressedRadixTree a -> Text -> Maybe (Text, a)
  lookup (CompressedRadixTree arr0 rt0) = lookup1 rt0
    where
      fromSlice (TextSlice offs16 len16) = TI.text arr0 offs16 len16

      lookup1 rt !t
        | T.null t = case rt of
            CompressedRadixAccept v _ a -> Just (fromSlice v, a)
            CompressedRadixSkip _       -> Nothing
        | otherwise = case rt of
            CompressedRadixAccept _ ns _ -> lookupCompressedRadixNodes t ns
            CompressedRadixSkip     ns   -> lookupCompressedRadixNodes t ns

      lookupCompressedRadixNodes !t v = go 0
        where
          !vlen = V.length v
          go !i
            | i < vlen  = case V.unsafeIndex v i of
                CompressedRadixNode pfix rt -> case T.commonPrefixes (fromSlice pfix) t of
                  Just (_, remPfx, remSfx)
                    | T.null remPfx -> lookup1 rt remSfx
                    | otherwise     -> Nothing
                  Nothing -> go (i + 1)
            | otherwise = Nothing