packages feed

bytetrie-0.1.0.0: src/Data/Trie/Word8.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}

{- | Tries with 'Bytes' (equiv. 'ByteArray') as keys.
This implementation is optimized for performing queries rather
than updating the structure repeatedly.
-}
module Data.Trie.Word8
  ( -- * Trie Type
    Trie
  , valid

    -- * Query

    -- ** Lookup
  , lookup
  , lookupTrie
  , lookupPrefixes

    -- ** Search
  , multiFindReplace
  , search
  , replace
  , stripPrefix
  , stripPrefixWithKey

    -- ** Size
  , null
  , size

    -- * Construction
  , empty
  , singleton

    -- ** Conversion
  , fromList
  , toList
  , foldl'
  , traverse_

    -- ** Insertion
  , insert
  , insertWith

    -- ** Deletion
  , delete

    -- ** Combine
  , union
  , unionWith
  , append
  , prepend
  ) where

import Prelude hiding (lookup, null)

import Control.Applicative ((<|>))
import Data.Bifunctor (first)
import Data.Bytes (Bytes, fromByteArray, toByteArray)
import Data.Bytes.Chunks (Chunks)
import Data.Map.Word8 (Map)
import Data.Maybe (isNothing)
import Data.Monoid (Any (Any), getAny)
import Data.Primitive.ByteArray (ByteArray, indexByteArray)
import Data.Word (Word8)

import qualified Data.Bytes as Bytes
import qualified Data.Bytes.Builder as Build
import qualified Data.Foldable as Foldable
import qualified Data.Map.Word8 as Map
import qualified Data.Maybe.Unpacked as U

{- | Tries implemented using a 256-entry bitmap as given in
"Data.Map.Word8".
This means that each branch point can be navigated with only
some bit manipulations and adding an offset.
On sparse data, this should save a lot of space relative to holding
a 256-entry pointer array.

This data type has 'Tip', 'Run', and 'Branch' nodes.
Branches always have at least two children,
and Runs always have at least one byte.
Leaves are 'Tip's.
Once the invariants are met (see below),
there is exactly one 'Trie' representation for each trie.

In each constructor, the @U.Maybe a@ is a possible entry;
it comes before any child bytes.

INVARIANT: The Run constructor never has a linear child.
           Linear nodes are those with no value and exactly one child,
           which in this implementation is only valueless runs.
INVARIANT: The Run constructor never has zero bytes.
INVARIANT: The Branch constructor has at least two children.
INVARIANT: No child of a node has size zero. That includes:
             The next node after a run is never null.
             No child of a branch is ever null.
-}
data Trie a
  = Tip {-# UNPACK #-} !(U.Maybe a)
  | -- ByteArray uses more copying on modification,
    -- but the data structures are smaller than with Bytes, making lookup faster
    UnsafeRun {-# UNPACK #-} !(U.Maybe a) {-# UNPACK #-} !ByteArray !(Trie a)
  | UnsafeBranch {-# UNPACK #-} !(U.Maybe a) !(Map (Trie a))
  deriving (Eq, Functor)

instance (Semigroup a) => Semigroup (Trie a) where (<>) = append
instance (Semigroup a) => Monoid (Trie a) where mempty = empty
instance (Show a) => Show (Trie a) where show = show . toList

{-# COMPLETE Tip, Run, Branch #-}

pattern Run :: U.Maybe a -> ByteArray -> Trie a -> Trie a
pattern Run v run next <- UnsafeRun v run next
  where
    Run v run next
      | null next = Tip v
      | Bytes.null (fromByteArray run) = next -- WARNING: throws away `v`, value/non-value from `next`
      | Just (run', next') <- fromLinear next =
          UnsafeRun v (run <> run') next'
      | otherwise = UnsafeRun v run next

pattern Branch :: U.Maybe a -> Map (Trie a) -> Trie a
pattern Branch v children <- UnsafeBranch v children
  where
    Branch v (removeEmptyChildren -> children)
      | Map.null children = Tip v
      | Just (c, child) <- fromSingletonMap children =
          Run v (toByteArray $ Bytes.singleton c) child
      | otherwise = UnsafeBranch v children
removeEmptyChildren :: Map (Trie a) -> Map (Trie a)
removeEmptyChildren = Map.foldrWithKeys f Map.empty
 where
  f k v xs = if null v then xs else Map.insert k v xs

-- Get nodes with no value, and exactly one possible next byte.
-- I.e. it never returns an empty bytes in the tuple.
fromLinear :: Trie a -> Maybe (ByteArray, Trie a)
fromLinear (Run U.Nothing run next) = Just (run, next)
fromLinear _ = Nothing

valid :: Trie a -> Bool
valid (Tip _) = True
valid (Run _ run next) =
  not (Bytes.null (fromByteArray run))
    && isNothing (fromLinear next)
    && not (null next)
    && valid next
valid (Branch _ children) =
  Map.size children > 1
    && Map.foldrWithKeys nonNullChild True children
 where
  nonNullChild _ child !acc = acc && not (null child)

------------ Find/Replace ------------

{- | The raison-d'etre of this library: repeatedly search in a byte string
for the longest of multiple patterns and make replacements.
-}
multiFindReplace ::
  (Semigroup b) =>
  -- | construct a portion of the result from unmatched bytes
  (Bytes -> b) ->
  -- | construct a replacement from the found value
  (a -> b) ->
  -- | the dictionary of all replacements
  Trie a ->
  -- | input to be edited
  Bytes ->
  -- | result of replacement
  b
{-# INLINE multiFindReplace #-}
multiFindReplace fromNoMatch fromMatch = \t ->
  let needles = delete mempty t
      -- `into` counts up until the first index where a replacement is found
      go !into rawInp =
        let inp = Bytes.unsafeDrop into rawInp
            unMatched = Bytes.unsafeTake into rawInp
         in if
              | Bytes.null inp -> fromNoMatch unMatched
              | Just (val, rest) <- stripPrefix needles inp ->
                  fromNoMatch unMatched <> fromMatch val <> go 0 rest
              | otherwise -> go (into + 1) rawInp
   in go 0

replace :: Trie Bytes -> Bytes -> Chunks
replace t inp = Build.run 4080 $ go t inp
 where
  go = multiFindReplace Build.bytes Build.bytes

search :: Trie a -> Bytes -> Bool
search t inp = getAny $ go t inp
 where
  go = multiFindReplace (const mempty) (const $ Any True)

------------ Construction ------------

-- | The empty trie.
empty :: Trie a
empty = Tip U.Nothing

-- | A trie with a single element.
singleton :: Bytes -> a -> Trie a
singleton k v = prepend k $ Tip (U.Just v)

{- | Prepend every key in the 'Trie' with the given 'Bytes'.

This should be used internally instead of the Run ctor,
thereby ensuring the run length >= 2 invariant is maintained.
It is exported anyway because someone may find it useful.
-}
prepend :: Bytes -> Trie a -> Trie a
prepend bytes next = Run U.Nothing (toByteArray bytes) next

{- | Insert a new key/value into the trie.
If the key is already present in the trie, the associated value is
replaced with the new one.
'insert' is equivalent to 'insertWith' 'const'.
-}
insert :: Bytes -> a -> Trie a -> Trie a
insert = insertWith const

{- | Insert with a function, combining new value and old value.
@'insertWith' f key value trie@ will insert the pair @(key, value)@
into @trie@ if @key@ does not exist in the trie.
If the key does exist, the function will insert the pair
@(key, f new_value old_value)@.
-}
insertWith :: (a -> a -> a) -> Bytes -> a -> Trie a -> Trie a
insertWith f k v = unionWith f (singleton k v)

delete :: Bytes -> Trie a -> Trie a
delete k0 trie = go k0 trie
 where
  -- `go` is not always tail-recursive.
  -- Instead, each node with exactly one child must be checked after the
  --  deletion to ensure that child is non-empty.
  -- TODO
  -- However, as soon as it is known that the size must be greater than one,
  --  we can throw away all queued normalizations so far.
  -- Therefore, we maintain a delimited continuation as an accumulator,
  --  but I'm not yet sure how to manually store it.
  -- go :: Bytes -> Trie a
  go key node@(Tip v)
    | Bytes.null key
    , U.Just _ <- v -- NOTE this is redundant now, but when I use cps, it won't be
      =
        empty
    | otherwise = node
  go key node@(Run v (fromByteArray -> run) next)
    -- found key, therefore delete
    | Bytes.null key
    , U.Just _ <- v -- NOTE this is redundant now, but when I use cps, it won't be
      =
        prepend run next
    -- carry on searching for the key
    | Just key' <- Bytes.stripPrefix run key =
        Run v (toByteArray run) (go key' next)
    -- key not present
    | otherwise = node
  go key node@(Branch v children)
    -- found key, therefore delete
    | Bytes.null key
    , U.Just _ <- v =
        UnsafeBranch U.Nothing children
    -- carry on searching for the key
    | Just (c, key') <- Bytes.uncons key
    , Just child <- Map.lookup c children =
        Branch v (Map.insert c (go key' child) children)
    -- key not present
    | otherwise = node

{- | Union of the two tries, but where a key appears in both,
the associated values are combined with '(<>)' to produce the new value,
i.e. @append == unionWith (<>)@.
-}
append :: (Semigroup a) => Trie a -> Trie a -> Trie a
append = unionWith (<>)

{- | The left-biased union of the two tries.
It prefers the first when duplicate keys are encountered,
i.e. @union == unionWith const@.
-}
union :: Trie a -> Trie a -> Trie a
union = unionWith const

-- | Union with a combining function.
unionWith :: (a -> a -> a) -> Trie a -> Trie a -> Trie a
unionWith f trieA trieB = case (trieA, trieB) of
  (Tip a, Tip b) -> Tip (a `mergeValue` b)
  (Tip a, Run b run next) -> UnsafeRun (a `mergeValue` b) run next
  (Run a run next, Tip b) -> UnsafeRun (a `mergeValue` b) run next
  (Tip a, Branch b children) -> UnsafeBranch (a `mergeValue` b) children
  (Branch a children, Tip b) -> UnsafeBranch (a `mergeValue` b) children
  -- all non-Tip cases
  (Branch a children, Branch b children') ->
    UnsafeBranch (a `mergeValue` b) (mergeChildren children children')
  (Branch a children, r@(Run _ _ _)) ->
    UnsafeBranch (a `mergeValue` b) (mergeChildren children children')
   where
    (b, c, child') = unsafeUnconsRun r
    children' = Map.singleton c child'
  (r@(Run _ _ _), Branch b children') ->
    UnsafeBranch (a `mergeValue` b) (mergeChildren children children')
   where
    (a, c, child') = unsafeUnconsRun r
    children = Map.singleton c child'
  (Run a (fromByteArray -> run) next, Run b (fromByteArray -> run') next') ->
    if Bytes.null common
      then
        let mkChild bytes trie = case Bytes.uncons bytes of
              Prelude.Just (c, k) -> Map.singleton c (prepend k trie)
              Prelude.Nothing -> error "invariant violation: empty run bytes"
            child = mkChild run next
            child' = mkChild run' next'
         in UnsafeBranch (a `mergeValue` b) $ child `Map.union` child'
      else
        let child = prepend (uncommon run) next
            child' = prepend (uncommon run') next'
         in Run (a `mergeValue` b) (toByteArray common) $ unionWith f child child'
   where
    common = Bytes.longestCommonPrefix run run'
    uncommon bytes = Bytes.unsafeDrop (Bytes.length common) bytes
 where
  mergeChildren left right = Map.unionWith (unionWith f) left right
  mergeValue U.Nothing U.Nothing = U.Nothing
  mergeValue (U.Just x) (U.Just y) = U.Just (f x y)
  mergeValue x y = x <|> y

------------ Conversion ------------

{- | Build a trie from a list of key/value pairs.
If more than one value for the same key appears, the last value for that
key is retained.
-}
fromList :: [(Bytes, a)] -> Trie a
fromList kvs = Foldable.foldl' (\xs (k, v) -> insert k v xs) empty kvs

{- | Convert the trie to a list of key/value pairs.
The resulting list has its keys sorted in ascending order.
-}
toList :: Trie a -> [(Bytes, a)]
toList = \case
  Tip valO -> fromValue valO
  Run valO run next -> fromValue valO ++ prependList (fromByteArray run) (toList next)
  Branch valO children -> fromValue valO ++ Map.foldrWithKeys f [] children
   where
    f k v acc = prependList (Bytes.singleton k) (toList v) ++ acc
 where
  fromValue valO = (mempty,) <$> U.maybeToList valO
  prependList run list = first (run <>) <$> list

foldl' :: (b -> a -> b) -> b -> Trie a -> b
{-# INLINE foldl' #-}
foldl' f !b0 t0 = go b0 t0
 where
  go !b t = case t of
    Tip valO -> case valO of
      U.Just x -> f b x
      _ -> b
    Run valO _ next ->
      let b' = case valO of
            U.Just x -> f b x
            _ -> b
       in go b' next
    Branch valO children ->
      let b' = case valO of
            U.Just x -> f b x
            _ -> b
       in Map.foldl' go b' children

traverse_ :: (Applicative m) => (a -> m b) -> Trie a -> m ()
{-# INLINE traverse_ #-}
traverse_ f t0 = go t0
 where
  go t = case t of
    Tip valO -> case valO of
      U.Just x -> f x *> pure ()
      _ -> pure ()
    Run valO _ next -> case valO of
      U.Just x -> f x *> go next
      _ -> go next
    Branch valO children -> case valO of
      U.Just x -> f x *> Map.traverse_ go children
      _ -> Map.traverse_ go children

------------ Query ------------

-- | Lookup the value at the 'Bytes' key in the trie.
lookup :: Bytes -> Trie a -> Maybe a
lookup k (Tip v)
  | Bytes.null k = U.toBaseMaybe v
  | otherwise = Nothing
lookup k (Run v (fromByteArray -> run) next)
  | Bytes.null k = U.toBaseMaybe v
  | run `Bytes.isPrefixOf` k =
      let k' = Bytes.unsafeDrop (Bytes.length run) k
       in lookup k' next
  | otherwise = Prelude.Nothing
lookup k (Branch valO children) = case Bytes.uncons k of
  Prelude.Nothing -> U.toBaseMaybe valO
  Prelude.Just (c, k') -> lookup k' =<< Map.lookup c children

{- | Lookup the value at the 'Bytes' key in the trie. Returns the value
of the exact match and the values for any keys that are prefixes of
the search key. The shortest prefix is first. The exact match (if there
is one) is last.
-}
lookupPrefixes :: Bytes -> Trie a -> [a]
{-# INLINE lookupPrefixes #-}
lookupPrefixes = lookupPrefixesGo []

lookupPrefixesGo :: [a] -> Bytes -> Trie a -> [a]
lookupPrefixesGo !acc !_ (Tip v)
  | U.Just x <- v = x : acc
  | otherwise = acc
lookupPrefixesGo !acc !k (UnsafeRun v (fromByteArray -> run) next) =
  let acc' = case v of
        U.Just x -> x : acc
        _ -> acc
   in if
        | Bytes.null k -> acc'
        | run `Bytes.isPrefixOf` k ->
            let k' = Bytes.unsafeDrop (Bytes.length run) k
             in lookupPrefixesGo acc' k' next
        | otherwise -> acc'
lookupPrefixesGo !acc !k (UnsafeBranch valO children) =
  let acc' = case valO of
        U.Just x -> x : acc
        _ -> acc
   in case Bytes.uncons k of
        Prelude.Nothing -> acc'
        Prelude.Just (c, k') -> case Map.lookup c children of
          Nothing -> acc'
          Just child -> lookupPrefixesGo acc' k' child

{- | Lookup the trie at the 'Bytes' key in the trie. Returns the subtrie
at this position.

>>> (k1 <> k2 == k) ==> (lookup k v t == lookup k2 (lookupTrie k1 t))
-}
lookupTrie :: Bytes -> Trie a -> Trie a
lookupTrie !k trie
  | Bytes.null k = trie
  | otherwise = case trie of
      Tip {} -> empty
      Run _ (fromByteArray -> run) next
        | run `Bytes.isPrefixOf` k ->
            let k' = Bytes.unsafeDrop (Bytes.length run) k
             in lookupTrie k' next
        | k `Bytes.isPrefixOf` run ->
            let run' = Bytes.unsafeDrop (Bytes.length k) run
             in Run U.Nothing (Bytes.toByteArrayClone run') next
        | otherwise -> empty
      Branch _ children ->
        let !k' = Bytes.unsafeDrop 1 k
            !c = Bytes.unsafeIndex k 0
         in case Map.lookup c children of
              Nothing -> empty
              Just child -> lookupTrie k' child

{- | Find the longest prefix of the input 'Bytes' which has a value in the trie.
Returns the associated value and the remainder of the input after the prefix.
-}
stripPrefix :: Trie a -> Bytes -> Maybe (a, Bytes)
stripPrefix trie inp = first snd <$> stripPrefixWithKey trie inp

{- | Find the longest prefix of the input 'Bytes' which has a value in the trie.
Returns the prefix and associated value found as a key/value tuple,
and also the remainder of the input after the prefix.
-}
stripPrefixWithKey :: forall a. Trie a -> Bytes -> Maybe ((Bytes, a), Bytes)
stripPrefixWithKey trie0 rawInp = go 0 Nothing trie0
 where
  go :: Int -> Maybe (Bytes, a) -> Trie a -> Maybe ((Bytes, a), Bytes)
  go !into !prior node =
    let inp = Bytes.unsafeDrop into rawInp
        candidate = (Bytes.unsafeTake into rawInp,) <$> topValue node
        found = candidate <|> prior
     in if
          | Run _ (fromByteArray -> run) next <- node
          , run `Bytes.isPrefixOf` inp ->
              go (into + Bytes.length run) found next
          | Branch _ children <- node
          , Just (c, _) <- Bytes.uncons inp
          , Just next <- Map.lookup c children ->
              go (into + 1) found next
          | otherwise -> mkReturn <$> found
  mkReturn (prefix, v) =
    let post = Bytes.unsafeDrop (Bytes.length prefix) rawInp
     in ((prefix, v), post)

null :: Trie a -> Bool
null (Tip U.Nothing) = True
null _ = False

size :: Trie a -> Int
size node = here + under
 where
  here = maybe 0 (const 1) (topValue node)
  under = case node of
    Tip _ -> 0
    Run _ _ next -> size next
    Branch _ children -> Map.foldrWithKeys (\_ child !acc -> acc + size child) 0 children

------ Helpers ------

topValue :: Trie a -> Maybe a
topValue = \case
  Tip v -> U.toBaseMaybe v
  Run v _ _ -> U.toBaseMaybe v
  Branch v _ -> U.toBaseMaybe v

unsafeUnconsRun :: Trie a -> (U.Maybe a, Word8, Trie a)
unsafeUnconsRun (Run v0 bs next) = (v0, c, run')
 where
  c = indexByteArray bs 0
  bs' = Bytes.unsafeDrop 1 (fromByteArray bs)
  run' = prepend bs' next
unsafeUnconsRun (Tip _) = error "unsafeUnconsRun on Tip trie"
unsafeUnconsRun (Branch _ _) = error "unsafeUnconsRun on Branch trie"

-- TODO is this really a decent way to do this?
fromSingletonMap :: Map a -> Maybe (Word8, a)
fromSingletonMap mp = case Map.toList mp of
  [(c, v)] -> Just (c, v)
  _ -> Nothing