heidi-0.0.0: src/Heidi/Data/Row/GenericTrie.hs
{-# language DeriveFunctor #-}
{-# language DeriveFoldable #-}
{-# language DeriveGeneric #-}
{-# language DeriveTraversable #-}
{-# language TemplateHaskell #-}
{-# language LambdaCase #-}
{-# language RankNTypes #-}
{-# options_ghc -Wno-unused-imports #-}
-----------------------------------------------------------------------------
-- |
-- Module : Heidi.Data.Row.GenericTrie
-- Description : A sparse dataframe row, based on GenericTrie
-- Copyright : (c) Marco Zocca (2018-2019)
-- License : BSD-style
-- Maintainer : ocramz fripost org
-- Stability : experimental
-- Portability : GHC
--
-- Rows are internally represented with prefix trees ("tries"), as provided by the
-- @generic-trie@ library; in addition to supporting the possibility of missing features in the dataset, tries provide fast insertion and lookup functionality when keyed with structured datatypes (such as lists or trees).
--
-----------------------------------------------------------------------------
module Heidi.Data.Row.GenericTrie (
Row
-- * Construction
, rowFromList, empty
-- ** (unsafe)
, mkRow
-- * Update
, insert, insertMany, insertWith
-- * Access
, toList, keys
-- * Filtering
, delete, filterWithKey, filterWithKeyPrefix, filterWithKeyAny
, deleteMany
-- * Partitioning
, partitionWithKey, partitionWithKeyPrefix
-- -- ** Decoders
-- , real, scientific, text, string, oneHot
-- * Lookup
, lookup
-- , lookupThrowM
, (!:), elemSatisfies
-- ** Lookup utilities
, maybeEmpty
-- ** Comparison by lookup
, eqByLookup, eqByLookups
, compareByLookup
-- * Set operations
, union, unionWith
, intersection, intersectionWith
-- * Maps
, mapWithKey
-- * Folds
, foldWithKey, keysOnly
-- * Traversals
, traverseWithKey
-- * Lenses
, int, bool, float, double, char, string, text, scientific, oneHot
-- ** Lens combinators
, at, keep
-- *** Combinators for list-indexed rows
, atPrefix, eachPrefixed, foldPrefixed
) where
-- import Control.Monad (foldM
import Data.Functor.Const (Const(..))
import Data.Functor.Identity (Identity(..))
import Data.List (isPrefixOf)
import Data.Maybe (fromMaybe)
import Data.Monoid (Any(..), All(..))
-- import Data.Semigroup (Endo)
-- import Data.Typeable (Typeable)
-- import Control.Applicative (Alternative(..))
import qualified Data.Foldable as F
-- import Control.Monad (filterM)
-- generic-trie
import qualified Data.GenericTrie as GT
-- exceptions
-- import Control.Monad.Catch (MonadThrow(..))
-- microlens
import Lens.Micro (Lens', Traversal', Getting, (^.), (<&>), _Just, Getting, traversed, folded, to, has)
-- -- microlens-th
-- import Lens.Micro.TH (makeLenses)
-- scientific
import Data.Scientific (Scientific)
-- text
import Data.Text (Text)
-- import qualified Data.Generics.Decode as D (Decode, mkDecode)
-- import Data.Generics.Decode ((>>>))
import Data.Generics.Encode.Internal (VP, vpInt, vpFloat, vpDouble, vpString, vpChar, vpText, vpBool, vpScientific, vpOneHot)
import Data.Generics.Encode.OneHot (OneHot)
-- import Data.Generics.Codec
-- import Core.Data.Row.Internal (KeyError(..))
import Prelude hiding (any, lookup)
-- $setup
-- >>> import Data.Generics.Encode.Internal (VP)
-- >>> let row0 = fromList [(0, 'a'), (3, 'b')] :: Row Int Char
-- >>> let row1 = fromList [(0, 'x'), (1, 'b'), (666, 'z')] :: Row Int Char
-- | A 'Row' type is internally a Trie:
--
-- * Fast random access
-- * Fast set operations
-- * Supports missing elements
newtype Row k v = Row { _unRow :: GT.Trie k v } deriving (Functor, Foldable, Traversable)
-- makeLenses ''Row
instance (GT.TrieKey k, Show k, Show v) => Show (Row k v) where
show = show . GT.toList . _unRow
instance (GT.TrieKey k, Eq k, Eq v) => Eq (Row k v) where
r1 == r2 = F.toList r1 == F.toList r2
instance (GT.TrieKey k, Eq k, Eq v, Ord k, Ord v) => Ord (Row k v) where
r1 <= r2 = F.toList r1 <= F.toList r2
-- | Focus on a given column
at :: GT.TrieKey k => k -> Lens' (Row k a) (Maybe a)
at k f m = f mv <&> \case
Nothing -> maybe m (const (delete k m)) mv
Just v' -> insert k v' m
where mv = lookup k m
{-# INLINABLE at #-}
-- | 'atPrefix' : a Lens' that takes a key prefix and relates a row having lists as keys and the subset of columns corresponding to keys having that prefix
atPrefix :: (GT.TrieKey k, Eq k) =>
[k] -- ^ key prefix of the columns of interest
-> Lens' (Row [k] v) [v]
atPrefix k f m = f vs <&> \case
[] -> if null kvs then m else deleteMany ks m
vs' -> insertMany (zip ks vs') m
where
kvs = toList $ filterWithKeyPrefix k m
(ks, vs) = unzip kvs
{- | Focus on all elements that share a common key prefix
e.g.
@
>>> :t \k -> 'Lens.Micro.toListOf' (eachPrefixed k . 'vpBool')
(GT.TrieKey k, Eq k) => [k] -> Row [k] VP -> [Bool]
@
-}
eachPrefixed :: (GT.TrieKey k, Eq k) =>
[k] -- ^ key prefix of the columns of interest
-> Traversal' (Row [k] v) v
eachPrefixed k = atPrefix k . traversed
-- | Extract all elements that share a common key prefix into a monoidal value (e.g. a list)
foldPrefixed :: (GT.TrieKey k, Eq k, Monoid r) =>
[k] -- ^ key prefix of the columns of interest
-> Getting r (Row [k] v) v
foldPrefixed k = atPrefix k . folded
-- foldingPrefixed f k = atPrefix k . folding f
-- any :: Eq a => a -> a -> Any
-- any v = Any . (== v)
-- | Helper for filtering 'Frame's
--
-- e.g.
--
-- >>> :t \k -> keep (text k) (== "hello")
-- :: GT.TrieKey k => k -> Row k VP -> Bool
keep :: Getting Any row a
-> (a -> b) -- ^ e.g. a predicate
-> row
-> Bool
keep l f = has (l . to f)
-- keep :: (Eq a) => Getting Any row a -> a -> row -> Bool
-- keep l v = has (l . to (== v))
-- ** Lenses
-- | Decode a 'Bool' from the given column index
bool :: GT.TrieKey k => k -> Traversal' (Row k VP) Bool
bool k = at k . _Just . vpBool
-- | Decode a 'Int' from the given column index
int :: GT.TrieKey k => k -> Traversal' (Row k VP) Int
int k = at k . _Just . vpInt
-- | Decode a 'Float' from the given column index
float :: GT.TrieKey k => k -> Traversal' (Row k VP) Float
float k = at k . _Just . vpFloat
-- | Decode a 'Double' from the given column index
double :: GT.TrieKey k => k -> Traversal' (Row k VP) Double
double k = at k . _Just . vpDouble
-- | Decode a 'Char' from the given column index
char :: GT.TrieKey k => k -> Traversal' (Row k VP) Char
char k = at k . _Just . vpChar
-- | Decode a 'String' from the given column index
string :: GT.TrieKey k => k -> Traversal' (Row k VP) String
string k = at k . _Just . vpString
-- | Decode a 'Text' from the given column index
text :: GT.TrieKey k => k -> Traversal' (Row k VP) Text
text k = at k . _Just . vpText
-- | Decode a 'Scientific' from the given column index
scientific :: GT.TrieKey k => k -> Traversal' (Row k VP) Scientific
scientific k = at k . _Just . vpScientific
-- | Decode a 'OneHot' from the given column index
oneHot :: GT.TrieKey k => k -> Traversal' (Row k VP) (OneHot Int)
oneHot k = at k . _Just . vpOneHot
-- | Construct a 'Row' from a list of key-element pairs.
--
-- >>> lookup 3 (rowFromList [(3,'a'),(4,'b')])
-- Just 'a'
-- >>> lookup 6 (rowFromList [(3,'a'),(4,'b')])
-- Nothing
rowFromList :: GT.TrieKey k => [(k, v)] -> Row k v
rowFromList = Row . GT.fromList
-- | Construct a 'Row' from a trie (unsafe).
mkRow :: GT.Trie k v -> Row k v
mkRow = Row
-- | An empty row
empty :: GT.TrieKey k => Row k v
empty = Row GT.empty
-- | Access the key-value pairs contained in the 'Row'
toList :: GT.TrieKey k => Row k v -> [(k ,v)]
toList = GT.toList . _unRow
-- | Lookup the value stored at a given key in a row
--
-- >>> lookup 0 row0
-- Just 'a'
-- >>> lookup 1 row0
-- Nothing
lookup :: (GT.TrieKey k) => k -> Row k v -> Maybe v
lookup k = GT.lookup k . _unRow
liftLookup :: GT.TrieKey k =>
(a -> b -> c) -> k -> Row k a -> Row k b -> Maybe c
liftLookup f k r1 r2 = f <$> lookup k r1 <*> lookup k r2
-- | Compares for ordering two rows by the values indexed at a specific key.
--
-- Returns Nothing if the key is not present in either row.
compareByLookup :: (GT.TrieKey k, Eq k, Ord a) =>
k -> Row k a -> Row k a -> Maybe Ordering
compareByLookup = liftLookup compare
-- | Compares two rows by the values indexed at a specific key.
--
-- Returns Nothing if the key is not present in either row.
eqByLookup :: (GT.TrieKey k, Eq k, Eq a) =>
k -> Row k a -> Row k a -> Maybe Bool
eqByLookup = liftLookup (==)
-- | Compares two rows by the values indexed at a set of keys.
--
-- Returns Nothing if a key in either row is not present.
eqByLookups :: (Foldable t, GT.TrieKey k, Eq k, Eq a) =>
t k -> Row k a -> Row k a -> Maybe Bool
eqByLookups ks r1 r2 = F.foldlM insf True ks where
insf b k = (&&) <$> pure b <*> eqByLookup k r1 r2
-- -- | Like 'lookup', but throws a 'KeyError' if the lookup is unsuccessful
-- lookupThrowM :: (MonadThrow m, Show k, Typeable k, GT.TrieKey k) =>
-- k -> Row k v -> m v
-- lookupThrowM k r = maybe (throwM $ MissingKeyError k) pure (lookup k r)
-- | Returns an empty row if the argument is Nothing.
maybeEmpty :: GT.TrieKey k => Maybe (Row k v) -> Row k v
maybeEmpty = fromMaybe empty
-- | List the keys of a given row
--
-- >>> keys row0
-- [0,3]
keys :: GT.TrieKey k => Row k v -> [k]
keys = map fst . toList
-- | Takes the union of a Foldable container of 'Row's and discards the values
keysOnly :: (GT.TrieKey k, Foldable f) => f (Row k v) -> Row k ()
keysOnly ks = () <$ F.foldl' union empty ks
-- | Returns a new 'Row' that doesn't have a given key-value pair
delete :: GT.TrieKey k =>
k -- ^ Key to remove
-> Row k v
-> Row k v
delete k (Row gt) = Row $ GT.delete k gt
-- | Produce a new 'Row' such that its keys do _not_ belong to a certain set.
deleteMany :: (GT.TrieKey k, Foldable t) => t k -> Row k v -> Row k v
deleteMany ks r = foldl (flip delete) r ks
-- | Map over all elements with a function of both the key and the value
mapWithKey :: GT.TrieKey k => (k -> a -> b) -> Row k a -> Row k b
mapWithKey ff (Row gt) =
runIdentity $ Row <$> GT.traverseWithKey (\k v -> pure (ff k v)) gt
-- | Filter a row by applying a predicate to its keys and corresponding elements.
--
-- NB : filtering _retains_ the elements that satisfy the predicate.
filterWithKey :: GT.TrieKey k => (k -> v -> Bool) -> Row k v -> Row k v
filterWithKey ff (Row gt) = Row $ GT.filterWithKey ff gt
-- | Retains the entries for which the given list is a prefix of the indexing key
filterWithKeyPrefix :: (GT.TrieKey a, Eq a) =>
[a] -- ^ key prefix
-> Row [a] v
-> Row [a] v
filterWithKeyPrefix kpre = filterWithKey (\k _ -> kpre `isPrefixOf` k)
-- | Partition a 'Row' into two new ones, such as the elements that satisfy the predicate will end up in the _left_ row.
partitionWithKey :: GT.TrieKey k =>
(k -> v -> Bool) -- ^ predicate
-> Row k v
-> (Row k v, Row k v)
partitionWithKey qf = foldWithKey insf (empty, empty)
where
insf k v (lacc, racc) | qf k v = (insert k v lacc, racc)
| otherwise = (lacc, insert k v racc)
-- | Uses 'partitionWithKey' internally
partitionWithKeyPrefix :: (GT.TrieKey a, Eq a) =>
[a] -- ^ key prefix
-> Row [a] v
-> (Row [a] v, Row [a] v)
partitionWithKeyPrefix kpre = partitionWithKey (\k _ -> kpre `isPrefixOf` k)
-- | Retains the entries for which the given item appears at any position in the indexing key
filterWithKeyAny :: (GT.TrieKey a, Eq a) => a -> Row [a] v -> Row [a] v
filterWithKeyAny kany = filterWithKey (\k _ -> kany `elem` k)
-- alter k m = fromMaybe m $ do
-- v <- lookup k m
-- delete k m
-- alter k f t =
-- case f (lookup k t) of
-- Just v' -> insert k v' t
-- Nothing -> delete k t
-- modify k v =
-- | Insert a key-value pair into a row and return the updated one
--
-- >>> keys $ insert 2 'y' row0
-- [0,2,3]
insert :: (GT.TrieKey k) => k -> v -> Row k v -> Row k v
insert k v = Row . GT.insert k v . _unRow
insertMany :: (GT.TrieKey k, Foldable t) => t (k, v) -> Row k v -> Row k v
insertMany kvs r = foldl (\acc (k, v) -> insert k v acc) r kvs
-- | Insert a key-value pair into a row and return the updated one, or updates the value by using the combination function.
insertWith :: (GT.TrieKey k) => (v -> v -> v) -> k -> v -> Row k v -> Row k v
insertWith f k v = Row . GT.insertWith f k v . _unRow
-- | Fold over a row with a function of both key and value
foldWithKey :: GT.TrieKey k => (k -> a -> r -> r) -> r -> Row k a -> r
foldWithKey fk z (Row gt) = GT.foldWithKey fk z gt
-- | Traverse a 'Row' using a function of both the key and the element.
traverseWithKey :: (Applicative f, GT.TrieKey k) => (k -> a -> f b) -> Row k a -> f (Row k b)
traverseWithKey f r = Row <$> GT.traverseWithKey f (_unRow r)
-- | Set union of two rows
--
-- >>> keys $ union row0 row1
-- [0,1,3,666]
union :: (GT.TrieKey k) => Row k v -> Row k v -> Row k v
union r1 r2 = Row $ GT.union (_unRow r1) (_unRow r2)
-- | Set union of two rows, using a combining function for equal keys
unionWith :: (GT.TrieKey k) =>
(v -> v -> v) -> Row k v -> Row k v -> Row k v
unionWith f r1 r2 = Row $ GT.unionWith f (_unRow r1) (_unRow r2)
-- | Set intersection of two rows
intersection :: GT.TrieKey k => Row k v -> Row k b -> Row k v
intersection r1 r2 = Row $ GT.intersection (_unRow r1) (_unRow r2)
-- | Set intersections of two rows, using a combining function for equal keys
intersectionWith :: GT.TrieKey k => (a -> b -> v) -> Row k a -> Row k b -> Row k v
intersectionWith f r1 r2 = Row $ GT.intersectionWith f (_unRow r1) (_unRow r2)
-- | Looks up a key from a row and applies a predicate to its value (if this is found). If no value is found at that key the function returns False.
--
-- This function is meant to be used as first argument to 'filter'.
--
-- >>> elemSatisfies (== 'a') 0 row0
-- True
-- >>> elemSatisfies (== 'a') 42 row0
-- False
elemSatisfies :: (GT.TrieKey k) => (a -> Bool) -> k -> Row k a -> Bool
elemSatisfies f k row = maybe False f (lookup k row)
-- | Inline synonym for 'elemSatisfies'
(!:) :: (GT.TrieKey k) => k -> (a -> Bool) -> Row k a -> Bool
k !: f = elemSatisfies f k
-- -- | Lookup a value from a Row indexed at the given key (returns in a MonadThrow type)
-- lookupColM :: (MonadThrow m, Show k, Typeable k, GT.TrieKey k) =>
-- k -> D.Decode m (Row k o) o
-- lookupColM k = D.mkDecode (lookupThrowM k)
-- -- -- | Lookup a value from a Row indexed at the given key (returns in the Maybe monad)
-- -- lookupCol :: GT.TrieKey k => k -> D.Decode Maybe (Row k o) o
-- -- lookupCol k = D.mkDecode (lookup k)
-- -- * Decoders
-- -- | Lookup and decode a real number
-- real :: (MonadThrow m, Show k, Typeable k, GT.TrieKey k, Alternative m) =>
-- k -> D.Decode m (Row k VP) Double
-- real k = lookupColM k >>> realM
-- -- | Lookup and decode a real 'Scientific' value
-- scientific :: (MonadThrow m, Show k, Typeable k, GT.TrieKey k, Alternative m) =>
-- k -> D.Decode m (Row k VP) Scientific
-- scientific k = lookupColM k >>> scientificM
-- -- | Lookup and decode a text string (defaults to Text)
-- text :: (MonadThrow m, Show k, Typeable k, GT.TrieKey k, Alternative m) =>
-- k -> D.Decode m (Row k VP) Text
-- text k = lookupColM k >>> textM
-- -- | Lookup and decode a text string (defaults to 'String')
-- string :: (MonadThrow m, Show k, Typeable k, GT.TrieKey k, Alternative m) =>
-- k -> D.Decode m (Row k VP) String
-- string k = lookupColM k >>> stringM
-- -- | Lookup and decode a one-hot encoded enum
-- oneHot :: (MonadThrow m, Show k, Typeable k, GT.TrieKey k) =>
-- k -> D.Decode m (Row k VP) (OneHot Int)
-- oneHot k = lookupColM k >>> oneHotM
-- -- spork k1 k2 = (>) <$> real k1 <*> real k2