packages feed

defaultable-map (empty) → 1.0.0

raw patch · 4 files changed

+550/−0 lines, 4 filesdep +basedep +containersdep +deepseq

Dependencies added: base, containers, deepseq, semigroupoids

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2022, Gabriella Gonzalez++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Gabriella Gonzalez nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ defaultable-map.cabal view
@@ -0,0 +1,37 @@+cabal-version:      3.0+name:               defaultable-map+version:            1.0.0+synopsis:           Applicative maps+description:        This package provides a @Defaultable@ type constructor that+                    wraps any @Map@-like type to add an optional default value. +                    Wrapping a @Map@-like type in this way permits a valid+                    @Applicative@ instance, so you can think of this as an+                    "@Applicative@ map" package.+                    .+                    This package provides both a concrete and a generalized API:+                    .+                    * The concrete API wraps @Data.Map@ for better performance+                      and type inference+                    .+                    * The generalized API works with any @Map@-like type+bug-reports:        https://github.com/Gabriella439/defaultable-map/issues+license:            BSD-3-Clause+license-file:       LICENSE+author:             Gabriella Gonzalez+copyright:          2022 Gabriella Gonzalez+maintainer:         GenuineGabby@gmail.com++source-repository head+    type:             git+    location:         https://github.com/Gabriella439/defaultable-map++library+    exposed-modules:  Defaultable.Map+                    , Defaultable.Map.Generalized+    build-depends:    base >= 4.14.3.0 && < 5+                    , containers < 0.7+                    , deepseq >= 1.4.0.0 && < 1.5+                    , semigroupoids < 5.4+    hs-source-dirs:   src+    default-language: Haskell2010+    ghc-options:      -Wall
+ src/Defaultable/Map.hs view
@@ -0,0 +1,405 @@+{-# LANGUAGE DeriveAnyClass        #-}+{-# LANGUAGE DeriveDataTypeable    #-}+{-# LANGUAGE DeriveGeneric         #-}+{-# LANGUAGE DeriveTraversable     #-}+{-# LANGUAGE DerivingStrategies    #-}+{-# LANGUAGE QuantifiedConstraints #-}+{-# LANGUAGE UndecidableInstances  #-}++-- | This module provides a `Defaultable` type constructor for extending+-- `Map`-like types with a valid `Applicative` and `Alternative` instance.  If+--  you're looking for an \"`Applicative` `Map`\" then you are in the right+--  place!+--+--  The `Defaultable` type constructor can be used to wrap any `Map`-like, such+--  as @"Data.Map".`Data.Map.Map`@ or @"Data.HashMap".`Data.HashMap.HashMap`@.+--+--  For convenience, this module also includes a concrete API wrapping+--  @"Data.Map".`Data.Map.Map`@ since that's the most common case.  If you+--  are interested in a more general API that works with other `Map` types then+--  check out the "Defaultable.Map.Generalized" module.+--+--  The `Applicative` instance enables the use of the @ApplicativeDo@ language+--  extension.  For example, suppose that you created the following three+--  `Defaultable` `Map`s:+--+-- @+-- firstNames, lastNames, handles :: `Defaultable` (`Map` `Int`) `String`+-- firstNames = `fromList` [(0, \"Gabriella\"    ), (1, \"Oscar\"), (2, \"Edgar\"    )                  ]+-- lastNames  = `fromList` [(0, \"Gonzalez\"     ),               (2, \"Codd\"     ), (3, \"Bryant\"   )]+-- handles    = `fromList` [(0, \"GabriellaG439\"), (1, \"posco\"),                   (3, \"avibryant\")]+-- @+--+--  Then you can use @ApplicativeDo@ notation to create an \"inner join\" of+--  these various maps, like this:+--+-- >>> :set -XApplicativeDo+-- >>> do firstName <- firstNames; lastName <- lastNames; return (firstName, lastName)+-- Defaultable (fromList [(0,("Gabriella","Gonzalez")),(2,("Edgar","Codd"))]) Nothing+--+-- … and you can join as many of these maps as you want by adding statements+-- to these @ApplicativeDo@ blocks:+--+-- @+-- {-# LANGUAGE ApplicativeDo #-}+--+-- innerJoins :: `Defaultable` (`Map` Int) (`String`, `String`, `String`)+-- innerJoins = do+--     firstName <- firstNames+--     lastName  <- lastNames+--     handles   <- handles+--     return (firstName, lastName, handles)+-- @+--+-- >>> innerJoins+-- Defaultable (fromList [(0,("Gabriella","Gonzalez","GabriellaG439"))]) Nothing+--+--  The `Alternative` instance for `Defaultable` is also important, too, because+--  you can use `Alternative` operations to create \"left/right joins\" and+--  something similar to an outer join, like this:+--+-- @+-- leftJoin :: `Defaultable` (`Map` `Int`) (`String`, `Maybe` `String`)+-- leftJoin = do+--     firstName <- firstNames+--     lastName  <- `Control.Applicative.optional` lastNames+--     return (firstName, lastName)+--+-- rightJoin :: `Defaultable` (`Map` `Int`) (`Maybe` `String`, `String`)+-- rightJoin = do+--     firstName <- `Control.Applicative.optional` firstNames+--     lastName  <- lastNames+--     return (firstName, lastName)+--+-- +-- similarToOuterJoin :: `Defaultable` (`Map` `Int`) (`Maybe` `String`, `Maybe` `String`)+-- similarToOuterJoin = do+--     firstName <- `Control.Applicative.optional` firstNames+--     lastName  <- `Control.Applicative.optional` lastNames+--     return (firstName, lastName)+-- @+--+-- >>> leftJoin+-- Defaultable (fromList [(0,("Gabriella",Just "Gonzalez")),(1,("Oscar",Nothing)),(2,("Edgar",Just "Codd"))]) Nothing+-- >>> rightJoin+-- Defaultable (fromList [(0,(Just "Gabriella","Gonzalez")),(2,(Just "Edgar","Codd")),(3,(Nothing,"Bryant"))]) Nothing+-- >>> similarToOuterJoin+-- Defaultable (fromList [(0,(Just "Gabriella",Just "Gonzalez")),(1,(Just "Oscar",Nothing)),(2,(Just "Edgar",Just "Codd")),(3,(Nothing,Just "Bryant"))]) (Just (Nothing,Nothing))+-- +-- You can also do more interesting multiway joins where any combiination of+-- the inputs may be `Control.Applicative.optional`:+-- +-- @+-- complexJoin :: `Defaultable` (`Map` `Int`) (`Maybe` `String`, `String`, `Maybe` `String`)+-- complexJoin = do+--     firstName <- `Control.Applicative.optional` firstNames+--     lastName  <- lastNames+--     handle    <- `Control.Applicative.optional` handles+--     return (firstName, lastName, handle)+-- @+--+-- >>> complexJoin+-- Defaultable (fromList [(0,(Just "Gabriella","Gonzalez",Just "GabrielG439")),(2,(Just "Edgar","Codd",Nothing)),(3,(Nothing,"Bryant",Just "avibryant"))]) Nothing++module Defaultable.Map+    ( -- * Comparison+      -- $comparison++      -- * Type+      Defaultable(..)+    , Map++      -- * Construction+    , fromMap+    , singleton+    , fromList+    , insert+    , withDefault++      -- * Query+    , lookup+    , toMap+    , toDefault+    ) where++import Control.Applicative (liftA2, Alternative(..))+import Control.DeepSeq (NFData)+import Data.Data (Data)+import Data.Functor.Alt (Alt(..))+import Data.Functor.Apply (Apply(..))+import Data.Map (Map)+import GHC.Generics (Generic, Generic1)+import Prelude hiding (lookup)++import qualified Data.Map as Map++-- $comparison+--+-- This package is similar to the+-- <https://hackage.haskell.org/package/total-map total-map package>,+-- which also provides an \"`Applicative` `Map`\" type.  However, there are a+-- couple of differences.+--+-- The first difference is that this package does not require you to supply a+-- default value in order to get a valid `Applicative` instance.  In other+-- words the default value is optional.  In contrast, the @total-map@ package+-- requires you to supply a default value.  That means that the `lookup`+-- function from this package can return `Nothing`, whereas the analogous+-- @(!)@ operator from the @total-map@ package always returns a value.+--+-- However, the benefit of this tradeoff is that this package can provide an+-- `Alternative` instance for `Defaultable`, whereas the @total-map@ package+-- does not have a valid `Alternative` instance.  Furthermore, the `Alternative`+-- instance enables support for left\/right\/\"outer\" joins as noted above.+--+-- Also, sometimes you just need an `Applicative` `Map` without a default value.+--+-- The other key difference compared to @total-map@ is that this package works+-- with `Map`-like types other than @"Data.Map".`Data.Map.Map`@, whereas+-- @total-map@ is hard-coded to @"Data.Map".`Data.Map.Map`@.  The only caveat+-- is that if you use the `Defaultable` type to wrap other `Map`-like types+-- (such as @"Data.HashMap".`Data.HashMap.HashMap`@) then you need to create+-- your own utility functions, such as a new `lookup` function for a+-- `Defaultable` `Data.HashMap.HashMap`.  However, this is not hard to do, as+-- you'll see if you consult the source code for each utility function.++{-| A `Defaultable` type is a @Map@-like type that is extended with an optional+    default value.  This default value can be used as a fallback if a lookup+    into the @Map@-like type does not find a matching key.++    The type variables are:++    * @map@: The @Map@-like type to wrap (typically including the type of key,+      but not the type of the value)++    * @value@ The type of each value stored in the @Map@-like type++    For example, you will typically have a type like+    @`Defaultable` (`Map` key) value@ or+    @`Defaultable` `Data.IntMap.IntMap` value@.++    You can build a `Defaultable` value using:++    * `empty` - The empty `Defaultable` has no keys and no default value+    * `pure` - A `Defaultable` with a default value but no keys+    * `fromMap` \/ `fromList` \/ `singleton` - Convenient construction functions+    * The `Defaultable` constructor++    You can transform and combine `Defaultable` values using:++    * (`<|>`) - Concatenate two `Defaultable` values, preferring keys and+      defaults from the right one+    * @do@ notation, if you enable @ApplicativeDo@+    * `withDefault` - To extend a `Defaultable` value with a default value++    You can query a `Defaultable` value using:++    * `lookup`+    * `toMap` / `toDefault`++    Note that the `Applicative` instance for this type is only valid for+    @map@ type constructors that satisfy the following extra law:++@+Given:++• mf :: map (a -> b)+• mx :: map a+• kf :: (a -> b) -> c+• kx :: a -> c++  `fmap` kf mf `<>` `fmap` kx mx `<>` (mf `<.>` mx)+= `fmap` kx mx `<>` `fmap` kf mf `<>` (mf `<.>` mx)+@++    … where `map` is the first type parameter that implements `Apply` and+    `Monoid`.++    The intuition here is if that @map@ is a `Map`-like type then we can think+    of those three expressions as having a set of keys associated with them,+    such that:++@+Given:++• keys :: map a -> `Data.Set.Set` key++keys (mf `<.>` mx) = keys (`fmap` f mx) \`intersection\` keys (`fmap` (`$` x) mf)+@++    So normally the following equality would not be true:++@+  `fmap` kf mf `<>` `fmap` kx mx+= `fmap` kx mx `<>` `fmap` kf mf+@++    … because the result would change if there was a key collision.  Then the+    order in which we union (`<>`) the two maps would change the result.++    However, if you union yet another map (@mf `<.>` mx@) that shadows the+    colliding keys then result remains the same.+-}+data Defaultable map value =+    Defaultable+        (map value)+        -- ^ The underlying @Map@-like type+        (Maybe value)+        -- ^ An optional default value to return if a key is missing+    deriving stock+        ( Data+        , Eq+        , Foldable+        , Functor+        , Generic+        , Generic1+        , Ord+        , Show+        , Traversable+        )+    deriving anyclass (NFData)++instance (Apply map, forall a . Monoid (map a)) => Apply (Defaultable map) where+    (<.>) = (<*>)++instance (Apply map, forall a . Monoid (map a)) => Applicative (Defaultable map) where+    pure v = Defaultable mempty (pure v)++    Defaultable fMap fDefault <*> Defaultable xMap xDefault =+        Defaultable fxMap fxDefault+      where+        fxMap = fFallback <> xFallback <> (fMap <.> xMap)+          where+            fFallback =+                case fDefault of+                    Nothing -> mempty+                    Just f  -> fmap f xMap++            xFallback =+                case xDefault of+                    Nothing -> mempty+                    Just x  -> fmap ($ x) fMap++        fxDefault = fDefault <*> xDefault++instance (Apply map, forall a . Monoid (map a)) => Alt (Defaultable map) where+    (<!>) = (<|>)++instance (Apply map, forall a . Monoid (map a)) => Alternative (Defaultable map) where+    empty = Defaultable mempty empty++    Defaultable lMap lDefault <|> Defaultable rMap rDefault =+        Defaultable (lMap <> rMap) (lDefault <|> rDefault)++-- | Not the same as the `Semigroup` instance for the underlying @map@ type+instance (Apply map, forall a . Monoid (map a), Semigroup value) => Semigroup (Defaultable map value) where+    (<>) = liftA2 (<>)++-- | Not the same as the `Monoid` instance for the underlying @map@ type+instance (Apply map, forall a . Monoid (map a), Monoid value) => Monoid (Defaultable map value) where+    mempty = pure mempty++{-| Create a `Defaultable` `Map` from a `Map`++>>> fromMap (Map.fromList [('A',1),('B',2)])+Defaultable (fromList [('A',1),('B',2)]) Nothing+-}+fromMap :: Map key value -> Defaultable (Map key) value+fromMap map_ = Defaultable map_ empty++{-| Create a `Defaultable` `Map` from a single key-value pair++>>> singleton ('A', 1)+Defaultable (fromList [('A',1)]) Nothing+-}+singleton :: (key, value) -> Defaultable (Map key) value+singleton (key, value) = fromMap (Map.singleton key value)++{-| Create a `Defaultable` `Map` from a list of key-value pairs++>>> fromList [('A',1),('B',2)]+Defaultable (fromList [('A',1),('B',2)]) Nothing+-}+fromList :: Ord key => [(key, value)] -> Defaultable (Map key) value+fromList pairs = fromMap (Map.fromList pairs)++{-| Insert a key-value pair into a `Defaultable` `Map`++>>> let example = fromList [('A', 1)]+>>> insert ('B', 2) example+Defaultable (fromList [('A',1),('B',2)]) Nothing++    For bulk updates, you should instead use `fromList`/`fromMap` with (`<|>`):++>>> fromList [('B', 2), ('C', 3)] <|> example+Defaultable (fromList [('A',1),('B',2),('C',3)]) Nothing+-}+insert+    :: Ord key+    => (key, value)+    -- ^+    -> Defaultable (Map key) value+    -- ^+    -> Defaultable (Map key) value+insert (key, value) (Defaultable map_ default_) =+    (Defaultable (Map.insert key value map_) default_)++{-| Add a default value to a `Defaultable` `Map` that is returned as a fallback+    if a `lookup` cannot find a matching key++>>> let example = fromList [('A',1)] `withDefault` 2+>>> lookup 'A' example+Just 1+>>> lookup 'B' example+Just 2+-}+withDefault+    :: Ord key+    => Defaultable (Map key) value+    -- ^+    -> value+    -- ^+    -> Defaultable (Map key) value+Defaultable map_ _ `withDefault` default_ = Defaultable map_ (Just default_)++{-| Lookup the value at a key in the map++    If the key is missing this falls back to returning the default value if+    present++    `lookup` is an `Monad` morphism, meaning that `lookup` distributes+    over `Monad` operatiorns:++@+`lookup` (`return` x) = `return` x++`lookup` (do x <- m; f x) = do x <- `lookup` m; `lookup` (f x)+@++    `lookup` is also an `Alternative` morphism, meaning that `lookup`+    distributes over `Alternative` operations:++@+`lookup` `empty` = `empty`++`lookup` (l `<|>` r) = `lookup` l `<|>` `lookup` r+@++>>> let example = fromList [('A',1)]+>>> lookup 'A' example+Just 1+>>> lookup 'B' example+Nothing+>>> lookup 'B' (example `withDefault` 2)+Just 2+-}+lookup :: Ord key => key -> Defaultable (Map key) value -> Maybe value+lookup key (Defaultable map_ default_) = Map.lookup key map_ <|> default_++-- | Extract the underlying map from a `Defaultable` map+toMap :: Defaultable (Map key) value -> Map key value+toMap (Defaultable map_ _) = map_++-- | Extract the default value from a `Defaultable` map+toDefault :: Defaultable (Map key) value -> Maybe value+toDefault (Defaultable _ default_) = default_
+ src/Defaultable/Map/Generalized.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE QuantifiedConstraints #-}++{-| This module exports an API that is similar to "Defaultable.Map", except+    the utilities have been generalized further to work with any+    `Data.Map.Map`-like type.++    The only utility that cannot be generalized in this way is+    `Defaultable.Map.lookup`, so that is the only function missing from this+    module.  Other than the missing `lookup` function, this module is a drop-in+    replacement for the "Defaultable.Map" module.++    Also, keep in mind that these generalized utilities may have worse type+    inference (especially you omit type annotations) and in some cases might+    also be more inefficient.  If this is an issue for you then you'll need to+    create your own local module specializing these utilities to your+    `Data.Map.Map`-like type of interest.+-}+module Defaultable.Map.Generalized+    ( Defaultable(..)++      -- * Construction+    , fromMap+    , singleton+    , fromList+    , insert+    , withDefault++      -- * Query+    , toMap+    , toDefault+    ) where++import Control.Applicative (empty, (<|>))+import Data.Functor.Apply (Apply)+import Defaultable.Map (Defaultable(..))+import GHC.Exts (IsList(Item))++import qualified GHC.Exts as Exts++-- | Generalized version of `Defaultable.Map.fromMap`+fromMap :: map value -> Defaultable map value+fromMap map_ = Defaultable map_ empty++-- | Generalized version of `Defaultable.Map.singleton`+singleton :: IsList (map value) => Item (map value) -> Defaultable map value+singleton item = fromList [ item ]++-- | Generalized version of `Defaultable.Map.fromList`+fromList :: IsList (map value) => [ Item (map value) ] -> Defaultable map value+fromList items = fromMap (Exts.fromList items)++-- | Generalized version of `Defaultable.Map.insert`+insert+    :: (IsList (map value), Apply map, forall a . Monoid (map a))+    => Item (map value)+    -- ^+    -> Defaultable map value+    -- ^+    -> Defaultable map value+insert item defaultable = defaultable <|> singleton item++-- | Generalized version of `Defaultable.Map.withDefault`+withDefault+    :: (Apply map, forall a . Monoid (map a))+    => Defaultable map value+    -- ^+    -> value+    -- ^+    -> Defaultable map value+defaultable `withDefault` default_ = defaultable <|> pure default_++-- | Generalized version of `Defaultable.Map.toMap`+toMap :: Defaultable map value -> map value+toMap (Defaultable map_ _) = map_++-- | Generalized version of `Defaultable.Map.toDefault`+toDefault :: Defaultable map value -> Maybe value+toDefault (Defaultable _ default_) = default_