packages feed

sdp-0.2: src/SDP/Map.hs

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances #-}
{-# LANGUAGE Safe, DefaultSignatures, ConstraintKinds, BangPatterns #-}

{- |
    Module      :  SDP.Map
    Copyright   :  (c) Andrey Mulik 2019
    License     :  BSD-style
    Maintainer  :  work.a.mulik@gmail.com
    Portability :  non-portable (GHC extensions)
    
    "SDP.Map" provides 'Map' - class of immutable associative arrays.
-}
module SDP.Map
(
  -- * Exports
  module SDP.Set,
  
  -- * Map
  Map (..), Map1, Map2
)
where

import Prelude ()
import SDP.SafePrelude
import SDP.Linear
import SDP.Set

import Data.List ( findIndex, findIndices )

import Control.Exception.SDP

default ()

infixl 9 .!, !, !?

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

{- |
  'Map' is a class of dictionaries, simple associative arrays with an arbitrary
  (implementation-dependent) key.
  
  In the current implementation, 'Map' (since @sdp-0.2@) is a superclass of
  'SDP.Indexed.Indexed'. 'Map' provides a set of operations on associative
  arrays that aren't specific to 'Linear' data structures and aren't limited by
  the 'Bordered' context (doesn't restrict key type).
-}
class (Nullable map) => Map map key e | map -> key, map -> e
  where
    {-# MINIMAL toMap', ((.!) | (!?)) #-}
    
    -- | Returns list of associations @(index, element)@.
    default assocs :: (Bordered map key, Linear map e) => map -> [(key, e)]
    assocs :: map -> [(key, e)]
    assocs es = indices es `zip` listL es
    
    {- |
      A less specific version of "SDP.Indexed.Indexed.assoc" that creates a new
      associative array. For 'Linear' structures without gaps, it may be less
      effective.
      
      > Z // ascs = toMap -- forall ascs
    -}
    toMap :: [(key, e)] -> map
    toMap =  toMap' (undEx "toMap {default}")
    
    {- |
      Strict version of 'toMap' with default value.
      
      Note that the default value is set only for elements included in the range
      of the created structure and will not be set for values outside its range
      (when expanding, concatenating, etc.) for most structures since they don't
      store it.
    -}
    toMap' :: e -> [(key, e)] -> map
    
    {- |
      @'insert'' key e map@ inserts @e@ with @key@ to @map@. If @map@ already
      contains an element with @key@, the element will be overwritten.
      
      If @map@ doesn't allow gaps, then the missing elements should be filled
      with default values.
    -}
    default insert' :: (Bordered map key) => key -> e -> map -> map
    insert' :: key -> e -> map -> map
    insert' k e es = toMap $ assocs es :< (k, e)
    
    {- |
      'delete'' removes element with given key.
      
      If the structure has boundaries, when removed from the beginning (end),
      they should change accordingly. If the structure doesn't allow gaps, then
      when removed from the middle, the actual value should be replaced with the
      default value.
    -}
    delete' :: key -> map -> map
    default delete' :: (Eq key) => key -> map -> map
    delete' k = toMap . except ((== k) . fst) . assocs
    
    -- | @'member'' key map@ checks if @key@ in @map@.
    default member' :: (Bordered map key) => key -> map -> Bool
    member' :: key -> map -> Bool
    member' =  flip indexIn
    
    -- | Update elements of immutable structure (by copying).
    (//) :: map -> [(key, e)] -> map
    (//) =  toMap ... (++) . assocs
    
    -- | @('.!')@ is unsafe reader, can be faster @('!')@ by skipping checks.
    {-# INLINE (.!) #-}
    (.!) :: map -> key -> e
    (.!) =  (undEx "(.!)" +?) ... (!?)
    
    -- | @('!')@ is well-safe reader, may 'throw' 'IndexException'.
    (!) :: map -> key -> e
    default (!) :: (Bordered map key) => map -> key -> e
    (!) es i = case inBounds (bounds es) i of
        IN -> es .! i
        ER -> empEx   msg
        OR -> overEx  msg
        UR -> underEx msg
      where
        msg = "(!) {default}"
    
    -- | @('!?')@ is completely safe, but very boring function.
    (!?) :: map -> key -> Maybe e
    (!?) es = flip member' es ?+ (es .!)
    
    -- | Filter with key.
    filter' :: (key -> e -> Bool) -> map -> map
    filter' f = toMap . filter (uncurry f) . assocs
    
    {- |
      'union'' is 'groupSetWith' for maps but works with real groups of
      elements, not with consequentive equal elements.
      
      'union'' merges/chooses elements with equal keys from two maps.
    -}
    union' :: (Ord key) => (e -> e -> e) -> map -> map -> map
    union' f = toMap ... on go assocs
      where
        go xs'@(x'@(i, x) : xs) ys'@(y'@(j, y) : ys) = case i <=> j of
          LT -> x' : go xs ys'
          EQ -> (i, f x y) : go xs ys
          GT -> y' : go xs' ys
        go xs'   Z = xs'
        go Z   ys' = ys'
    
    {- |
      @'difference'' f mx my@ applies @comb@ to values with equal keys.
      If @f x y@ (where @(k1, x) <- mx@, @(k2, y) <- my@, @k1 == k2@) is
      'Nothing', element isn't included to result map.
      
      Note that 'difference'' is poorer than a similar functions in containers.
    -}
    difference' :: (Ord key) => (e -> e -> Maybe e) -> map -> map -> map
    difference' f = toMap ... on go assocs
      where
        go xs'@(x'@(i, x) : xs) ys'@((j, y) : ys) = case i <=> j of
          GT -> go xs' ys
          LT -> x' : go xs ys'
          EQ -> case f x y of {(Just e) -> (i, e) : go xs ys; _ -> go xs ys}
        go xs' _ = xs'
    
    {- |
      @'intersection'' f mx my@ combines elements of 'intersection'' by @f@:
      if @isJust (f x y)@ (where @(k1, x) <- mx, (k2, y) <- my, k1 == k2@),
      then element is added to result map.
    -}
    intersection' :: (Ord key) => (e -> e -> e) -> map -> map -> map
    intersection' f = toMap ... on go assocs
      where
        go xs'@((i, x) : xs) ys'@((j, y) : ys) = case i <=> j of
          LT -> go xs ys'
          GT -> go xs' ys
          EQ -> (i, f x y) : go xs ys
        go _ _ = []
    
    -- | Update function, by default uses ('!') and may throw 'IndexException'.
    update :: map -> (key -> e -> e) -> map
    update es f = es // [ (i, f i e) | (i, e) <- assocs es ]
    
    {- |
      @lookupLT' k map@ finds pair @(key, value)@ with smallest @key@, where
      @key < k@ (if any). @k@ may not be a @map@ element.
    -}
    lookupLT' :: (Ord key) => key -> map -> Maybe (key, e)
    lookupLT' k = lookupLTWith cmpfst (k, unreachEx "lookupLT'") . assocs
    
    {- |
      @lookupGT' k map@ finds pair @(key, value)@ with greatest @key@, where
      @key > k@ (if any). @k@ may not be a @map@ element.
    -}
    lookupGT' :: (Ord key) => key -> map -> Maybe (key, e)
    lookupGT' k = lookupGTWith cmpfst (k, unreachEx "lookupGT'") . assocs
    
    {- |
      @lookupLE' k map@ finds pair @(key, value)@ with smallest @key@, where
      @key <= k@ (if any). If @k@ is a @map@ element, returns @(k, e)@.
    -}
    lookupLE' :: (Ord key) => key -> map -> Maybe (key, e)
    lookupLE' k me = (,) k <$> (me !? k) <|> lookupLEWith cmpfst (k, unreachEx "lookupLE'") (assocs me)
    
    {- |
      @lookupGE' k map@ finds pair @(key, value)@ with  @key@, where
      @key >= k@ (if any).
    -}
    lookupGE' :: (Ord key) => key -> map -> Maybe (key, e)
    lookupGE' k me = (,) k <$> (me !? k) <|> lookupGEWith cmpfst (k, unreachEx "lookupGE'") (assocs me)
    
    -- | Returns list of map keys.
    keys :: map -> [key]
    keys =  fsts . assocs
    
    -- | Searches the key of first matching element.
    (.$) :: (e -> Bool) -> map -> Maybe key
    (.$) =  null ?- head ... (*$)
    
    -- | Searches the keys of all matching elements.
    (*$) :: (e -> Bool) -> map -> [key]
    (*$) f = select (f . snd ?+ fst) . assocs
    
    {- Folds with key. -}
    
    -- | 'kfoldr' is 'foldr' with key.
    kfoldr :: (key -> e -> b -> b) -> b -> map -> b
    kfoldr f base = foldr (uncurry f) base . assocs
    
    -- | 'kfoldl' is 'foldl' with key.
    kfoldl :: (key -> b -> e -> b) -> b -> map -> b
    kfoldl f base = foldl (\ acc (i, e) -> f i acc e) base . assocs
    
    -- | 'kfoldr'' is strict version of 'kfoldr'.
    kfoldr' :: (key -> e -> b -> b) -> b -> map -> b
    kfoldr' f = kfoldr (\ !i e !b -> f i e b)
    
    -- | 'kfoldl'' is strict version of 'kfoldl'.
    kfoldl' :: (key -> b -> e -> b) -> b -> map -> b
    kfoldl' f = kfoldl (\ !i !b e -> f i b e)

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

-- | Kind @(* -> *)@ 'Map' structure.
type Map1 map key e = Map (map e) key e

-- | Kind @(* -> * -> *)@ 'Map' structure.
type Map2 map key e = Map (map key e) key e

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

instance Map [e] Int e
  where
    toMap' e = snds . fill . setWith cmpfst
      where
        fill (ix@(i1, _) : iy@(i2, _) : ies) =
          let rest = i1 + 1 == i2 ? iy : ies $ (i1 + 1, e) : iy : ies
          in  ix : fill rest
        fill xs = xs
    
    assocs = zip [0 ..] . listL
    
    insert' k e es = k < 0 ? es $ go k es
      where
        go 0    xs    = isNull xs ? [e] $ e : tail xs
        go i    []    = err : go (i - 1) []
        go i (x : xs) = x : go (i - 1) xs
        
        err = undEx "insert'"
    
    (x : xs) .! n = n == 0 ? x $ xs .! (n - 1)
    _        .! _ = error "in SDP.Map.(.!)"
    
    (!) [] _ = empEx "(!)"
    (!) es n = n >= 0 ? es !# n $ underEx "(!)"
      where
        []       !# _  = overEx "(!)"
        (x : xs) !# n' = n' == 0 ? x $ xs !# (n' - 1)
    
    []       !? _ = Nothing
    (x : xs) !? n = case n <=> 0 of
      GT -> xs !? (n - 1)
      EQ -> Just x
      LT -> Nothing
    
    xs // es = snds $ unionWith cmpfst (setWith cmpfst es) (assocs xs)
    
    (.$) = findIndex
    (*$) = findIndices
    
    kfoldr f base =
      let go i es = case es of {(x : xs) -> f i x $ go (i + 1) xs; _ -> base}
      in  go 0
    
    kfoldl f =
      let go i e es = case es of {(x : xs) -> go (i + 1) (f i e x) xs; _ -> e}
      in  go 0

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

empEx :: String -> a
empEx =  throw . EmptyRange . showString "in SDP.Map."

undEx :: String -> a
undEx =  throw . UndefinedValue . showString "in SDP.Map."

overEx :: String -> a
overEx =  throw . IndexOverflow . showString "in SDP.Map."

underEx :: String -> a
underEx =  throw . IndexUnderflow . showString "in SDP.Map."

unreachEx :: String -> a
unreachEx =  throw . UnreachableException . showString "in SDP.Map."