radix-tree-0.1: src/Data/RadixTree/Internal.hs
----------------------------------------------------------------------------
-- |
-- Module : Data.RadixTree.Internal
-- Copyright : (c) Sergey Vinokurov 2018
-- License : BSD3-style (see LICENSE)
-- Maintainer : serg.foo@gmail.com
--
-- This is an internal module that exposes innards of the 'RadixTree'
-- data structure. This API may change in any new release, even in a
-- patch release - depend on it at your own risk.
----------------------------------------------------------------------------
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_HADDOCK not-home #-}
module Data.RadixTree.Internal
( RadixTree(..)
, empty
, null
, size
, insert
, insertWith
, lookup
, fromList
, toList
, toAscList
, keys
, keysSet
, elems
, mapMaybe
, union
, unionWith
) where
import Prelude hiding (lookup, null)
import Control.Arrow (first)
import Control.DeepSeq
import Control.Monad.ST
import Control.Monad.ST.Unsafe
import Data.ByteString.Short (ShortByteString)
import qualified Data.ByteString.Short as BSS
import qualified Data.ByteString.Short.Internal as BSSI
import qualified Data.Foldable as Foldable
import Data.IntMap (IntMap)
import qualified Data.IntMap.Strict as IM
import qualified Data.List as L
import Data.Maybe (fromMaybe)
import Data.Primitive.ByteArray
import Data.Semigroup as Semigroup
import Data.Set (Set)
import qualified Data.Set as S
import Data.Word
import GHC.Generics (Generic)
-- | A tree data structure that efficiently indexes values by string keys.
--
-- This type can be more memory-efficient than 'Data.Map' because it combines
-- common prefixes of all keys. Specific savings will vary depending on
-- concrete data set.
data RadixTree a
= RadixNode
!(Maybe a)
!(IntMap (RadixTree a)) -- ^ Either has 0 or 2 or more children, never 1.
| RadixStr
!(Maybe a)
{-# UNPACK #-} !ShortByteString -- ^ Non-empty
!(RadixTree a)
deriving (Show, Functor, Foldable, Traversable, Generic)
instance NFData a => NFData (RadixTree a)
-- | Radix tree with no elements.
empty :: RadixTree a
empty = RadixNode Nothing IM.empty
{-# INLINE interleaveST #-}
interleaveST :: ST s a -> ST s a
interleaveST =
#if MIN_VERSION_base(4, 10, 0)
unsafeDupableInterleaveST
#else
unsafeInterleaveST
#endif
splitShortByteString :: Int -> ShortByteString -> (ShortByteString, ShortByteString, Word8, ShortByteString)
splitShortByteString n (BSSI.SBS source) = runST $ do
prefix <- newByteArray prefixSize
copyByteArray prefix 0 source' 0 prefixSize
ByteArray prefix# <- unsafeFreezeByteArray prefix
midSuffix <- interleaveST $ do
midSuffix <- newByteArray midSuffixSize
copyByteArray midSuffix 0 source' n midSuffixSize
unsafeFreezeByteArray midSuffix
suffix <- interleaveST $ do
suffix <- newByteArray suffixSize
copyByteArray suffix 0 source' (n + 1) suffixSize
unsafeFreezeByteArray suffix
pure (BSSI.SBS prefix#, byteArrayToBSS midSuffix, indexByteArray source' n, byteArrayToBSS suffix)
where
source' = ByteArray source
prefixSize = n
midSuffixSize = sizeofByteArray source' - prefixSize
suffixSize = midSuffixSize - 1
{-# INLINE byteArrayToBSS #-}
byteArrayToBSS :: ByteArray -> BSS.ShortByteString
byteArrayToBSS (ByteArray xs) = BSSI.SBS xs
dropShortByteString :: Int -> ShortByteString -> ShortByteString
dropShortByteString 0 src = src
dropShortByteString !n (BSSI.SBS source) = runST $ do
dest <- newByteArray sz
copyByteArray dest 0 source' n sz
byteArrayToBSS <$> unsafeFreezeByteArray dest
where
source' = ByteArray source
!sz = sizeofByteArray source' - n
singletonShortByteString :: Word8 -> ShortByteString
singletonShortByteString !c = runST $ do
dest <- newByteArray 1
writeByteArray dest 0 c
byteArrayToBSS <$> unsafeFreezeByteArray dest
{-# INLINE unsafeHeadeShortByteString #-}
unsafeHeadeShortByteString :: ShortByteString -> Word8
unsafeHeadeShortByteString = (`BSSI.unsafeIndex` 0)
data Mismatch
= IsPrefix
| CommonPrefixThenMismatch
!ShortByteString -- ^ Prefix of node contents common with the key
ShortByteString -- ^ Suffix with the first mismatching byte
Word8 -- ^ First byte of the suffix that caused mismatch
ShortByteString -- ^ Rest of node contents, suffix
deriving (Show, Generic)
analyseMismatch
:: ShortByteString -- ^ Key
-> Int -- ^ Key offset
-> ShortByteString -- ^ Node contents
-> Mismatch
analyseMismatch (BSSI.SBS key) !keyOffset nodeContentsBS@(BSSI.SBS nodeContents) =
case findMismatch 0 of
Nothing -> IsPrefix
Just mismatchIdx ->
case splitShortByteString mismatchIdx nodeContentsBS of
(prefix, midSuffix, mid, suffix) -> CommonPrefixThenMismatch prefix midSuffix mid suffix
where
keySize = sizeofByteArray key'
keyLeft = keySize - keyOffset
contentsSize = sizeofByteArray nodeContents'
key' = ByteArray key
nodeContents' = ByteArray nodeContents
limit :: Int
limit = min keyLeft contentsSize
findMismatch :: Int -> Maybe Int
findMismatch !i
| i == limit
= if i == contentsSize
then Nothing
else Just i -- Key ended in the middle of node's packed key.
| (indexByteArray key' (keyOffset + i) :: Word8) == indexByteArray nodeContents' i
= findMismatch $ i + 1
| otherwise
= Just i
mkRadixNodeFuse :: Maybe a -> IntMap (RadixTree a) -> Maybe (RadixTree a)
mkRadixNodeFuse val children =
case val of
Nothing | IM.null children
-> Nothing
val' | [(c, child)] <- IM.toList children
-> Just $ RadixStr val' (singletonShortByteString $ fromIntegral c) child
_ -> Just $ RadixNode val children
-- Precondition: input string is non-empty
mkRadixStrFuse :: Maybe a -> ShortByteString -> RadixTree a -> Maybe (RadixTree a)
mkRadixStrFuse val str rest =
case (val, rest) of
(val', RadixStr Nothing str' rest') ->
Just $ RadixStr val' (str Semigroup.<> str') rest'
(Nothing, node)
| null node -> Nothing
(val', rest') ->
Just $ RadixStr val' str rest'
mkRadixStr :: ShortByteString -> RadixTree a -> RadixTree a
mkRadixStr str rest
| BSS.null str = rest
| otherwise = RadixStr Nothing str rest
-- TODO: prove following function correct.
-- | Check whether radix tree is empty
null :: RadixTree a -> Bool
null = \case
RadixNode Nothing children -> IM.null children
RadixStr Nothing _ rest -> null rest
_ -> False
-- | O(n) Get number of elements in a radix tree.
size :: RadixTree a -> Int
size = length
-- | Add new element to a radix tree.
insert :: forall a. ShortByteString -> a -> RadixTree a -> RadixTree a
insert = insertWith const
-- | Add new element to a radix tree. If an element was already present for
-- the given key, use supplied funciton @f@ to produce a new value. The
-- function will be called like this @f newValue oldValue@.
insertWith :: forall a. (a -> a -> a) -> ShortByteString -> a -> RadixTree a -> RadixTree a
insertWith = insert'
{-# INLINE insert' #-}
insert' :: forall a. (a -> a -> a) -> ShortByteString -> a -> RadixTree a -> RadixTree a
insert' f key value = go 0
where
len = BSS.length key
readKey :: Int -> Int
readKey = fromIntegral . BSSI.unsafeIndex key
go :: Int -> RadixTree a -> RadixTree a
go i
| i < len
= \case
RadixNode oldValue children
| IM.null children ->
RadixStr oldValue (dropShortByteString i key) $ RadixNode (Just value) IM.empty
| otherwise ->
RadixNode oldValue $
IM.alter (Just . maybe optNode (go i')) c children
where
c :: Int
c = readKey i
i' = i + 1
optNode =
mkRadixStr (dropShortByteString i' key) $ RadixNode (Just value) IM.empty
RadixStr oldValue packedKey rest ->
case analyseMismatch key i packedKey of
IsPrefix ->
RadixStr oldValue packedKey $ go (i + BSS.length packedKey) rest
CommonPrefixThenMismatch prefix midSuffix mid suffix ->
(if BSS.null prefix then id else RadixStr oldValue prefix) $
if isKeyEnded
then
RadixStr (Just value) midSuffix rest
else
RadixNode (if BSS.null prefix then oldValue else Nothing) $
IM.fromList
[ ( mid'
, mkRadixStr suffix rest
)
, ( readKey i'
, mkRadixStr (dropShortByteString (i' + 1) key) $ RadixNode (Just value) IM.empty
)
]
where
i' = i + BSS.length prefix
isKeyEnded = i' >= len
mid' = fromIntegral mid
| otherwise
= \case
RadixNode oldValue children ->
RadixNode (Just (maybe value (f value) oldValue)) children
RadixStr oldValue key' rest ->
RadixStr (Just (maybe value (f value) oldValue)) key' rest
canStripPrefixFromShortByteString
:: Int -> ShortByteString -> ShortByteString -> Bool
canStripPrefixFromShortByteString bigStart (BSSI.SBS small) (BSSI.SBS big)
| bigStart + smallSize > bigSize = False
| otherwise = findMismatch 0
where
small' = ByteArray small
big' = ByteArray big
smallSize = sizeofByteArray small'
bigSize = sizeofByteArray big'
findMismatch :: Int -> Bool
findMismatch !i
| i == smallSize
= True
| (indexByteArray small' i :: Word8) == indexByteArray big' (bigStart + i)
= findMismatch $ i + 1
| otherwise
= False
-- | O(length(key)) Try to find a value associated with the given key.
lookup :: forall a. ShortByteString -> RadixTree a -> Maybe a
lookup key = go 0
where
len = BSS.length key
readKey :: Int -> Int
readKey = fromIntegral . BSSI.unsafeIndex key
go :: Int -> RadixTree a -> Maybe a
go !n tree
| n == len
= case tree of
RadixNode val _ -> val
RadixStr val _ _ -> val
| otherwise
= case tree of
RadixNode _ children ->
IM.lookup (readKey n) children >>= go (n + 1)
RadixStr _ packedKey rest
| canStripPrefixFromShortByteString n packedKey key
-> go (n + BSS.length packedKey) rest
| otherwise
-> Nothing
-- | Construct a radix tree from list of key-value pairs. If some key
-- appears twice in the input list, later occurrences will override
-- earlier ones.
fromList :: [(ShortByteString, a)] -> RadixTree a
fromList =
L.foldl' (\acc (k, v) -> insert' const k v acc) empty
-- | O(n) Convert a radix tree to a list of key-value pairs.
toList :: RadixTree a -> [(ShortByteString, a)]
toList = toAscList
-- | O(n) Convert a radix tree to an ascending list of key-value pairs.
toAscList :: forall a. RadixTree a -> [(ShortByteString, a)]
toAscList = map (first BSS.pack) . go
where
go :: RadixTree a -> [([Word8], a)]
go = \case
RadixNode val children ->
maybe id (\val' ys -> ([], val') : ys) val $
IM.foldMapWithKey (\c child -> map (first (fromIntegral c :)) $ go child) children
RadixStr val packedKey rest ->
maybe id (\val' ys -> ([], val') : ys) val $
map (first (BSS.unpack packedKey ++)) $
go rest
-- | O(n) Get all keys stored in a radix tree.
keys :: RadixTree a -> [ShortByteString]
keys = map BSS.pack . go
where
go :: RadixTree a -> [[Word8]]
go = \case
RadixNode val children ->
maybe id (\_ ys -> [] : ys) val $
IM.foldMapWithKey (\c child -> map (fromIntegral c :) $ go child) children
RadixStr val packedKey rest ->
maybe id (\_ ys -> [] : ys) val $
map (BSS.unpack packedKey <>) $
go rest
-- | O(n) Get set of all keys stored in a radix tree.
keysSet :: RadixTree a -> Set ShortByteString
keysSet = S.fromDistinctAscList . keys
-- | O(n) Get all values stored in a radix tree.
elems :: RadixTree a -> [a]
elems = Foldable.toList
-- | O(n) Map a function that can remove some existing elements over a
-- radix tree.
mapMaybe :: forall a b. (a -> Maybe b) -> RadixTree a -> RadixTree b
mapMaybe f = fromMaybe empty . go
where
go :: RadixTree a -> Maybe (RadixTree b)
go = \case
RadixNode val children ->
mkRadixNodeFuse (f =<< val) $ IM.mapMaybe go children
RadixStr val str rest ->
mkRadixStrFuse (f =<< val) str $ fromMaybe empty $ go rest
-- | O(n + m) Combine two radix trees trees. If a key is present in both
-- trees then the value from left one will be retained.
union :: RadixTree a -> RadixTree a -> RadixTree a
union = unionWith const
-- | O(n + m) Combine two trees using supplied function to resolve
-- values that have the same key in both trees.
unionWith :: forall a. (a -> a -> a) -> RadixTree a -> RadixTree a -> RadixTree a
unionWith f = go
where
combineVals :: Maybe a -> Maybe a -> Maybe a
combineVals x y = case (x, y) of
(Nothing, Nothing) -> Nothing
(Nothing, y'@Just{}) -> y'
(x'@Just{}, Nothing) -> x'
(Just x', Just y') -> Just $ f x' y'
go :: RadixTree a -> RadixTree a -> RadixTree a
go x y = case (x, y) of
(RadixNode val children, RadixNode val' children') ->
RadixNode (combineVals val val') (IM.unionWith go children children')
(RadixNode val children, RadixStr val' str' rest') ->
RadixNode (combineVals val val') $
(\g -> IM.alter g h children) $ \child ->
Just $!
let rest'' = mkRadixStr (dropShortByteString 1 str') rest' in
case child of
Nothing -> rest''
Just child' -> go child' rest''
where
h = fromIntegral $ unsafeHeadeShortByteString str'
(RadixStr val str rest, RadixNode val' children') ->
RadixNode (combineVals val val') $
(\g -> IM.alter g h children') $ \child ->
Just $!
let rest' = mkRadixStr (dropShortByteString 1 str) rest in
case child of
Nothing -> rest'
Just child' -> go rest' child'
where
h = fromIntegral $ unsafeHeadeShortByteString str
(RadixStr val str rest, RadixStr val' str' rest') ->
case analyseMismatch str 0 str' of
-- str' is a prefix of str
IsPrefix ->
RadixStr (combineVals val val') str' $
go (mkRadixStr (dropShortByteString (BSS.length str') str) rest) rest'
-- str' = prefix + firstMismatchStr' + suffixStr'
-- = prefix + midSuffixStr'
CommonPrefixThenMismatch prefix midSuffixStr' firstMismatchStr' suffixStr' ->
(if BSS.null prefix then id else RadixStr (combineVals val val') prefix) $
if BSS.length prefix == BSS.length str
then
go rest $ RadixStr
(if BSS.null prefix then combineVals val val' else Nothing)
midSuffixStr'
rest'
else RadixNode (if BSS.null prefix then combineVals val val' else Nothing) $ IM.fromList
[ ( fromIntegral firstMismatchStr'
, mkRadixStr suffixStr' rest'
)
, ( fromIntegral $ BSSI.unsafeIndex str $ BSS.length prefix
, mkRadixStr (dropShortByteString (BSSI.length prefix + 1) str) rest
)
]