cache-effectful (empty) → 0.0.1.0
raw patch · 7 files changed
+407/−0 lines, 7 filesdep +basedep +cachedep +cache-effectful
Dependencies added: base, cache, cache-effectful, effectful-core, hashable, tasty, tasty-hunit
Files
- CHANGELOG.md +4/−0
- LICENSE.md +20/−0
- README.md +38/−0
- cache-effectful.cabal +84/−0
- src/Effectful/Cache.hs +160/−0
- test/Main.hs +78/−0
- test/Utils.hs +23/−0
+ CHANGELOG.md view
@@ -0,0 +1,4 @@+# CHANGELOG++## v0.0.1.0 –+* Release
+ LICENSE.md view
@@ -0,0 +1,20 @@+Copyright (c) 2021 Hécate Moonlight++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,38 @@+# cache-effectful++A `Cache` effect for the [`effectful`][effectful] ecosystem.++## How to use++This library exposes the following elements:++* `Cache` — The type-level effect that you can declare in your type signatures.++```+populateIntCache :: (Cache Int Int :> es) => Eff es ()+```++* `insert`, `lookup`, `keys`, `delete`, `filterWithKey` – Operations on `Cache`. They should always be used with Type Applications when using literals:++```Haskell+insertAndLookup :: (Cache Int Int :> es) => Eff es (Maybe Int)+insertAndLookup = do+ insert @Int @Int 3 12+ lookup @Int 3++listKeys :: (Cache Int Int :> es) => Eff es [Int]+listKeys = do+ populateIntCache+ keys @Int @Int+```++* An IO Runner++```+runCacheIO (cache :: Data.Cache Int Int)+```++See the [tests][tests] to see an example use.++[effectful]: https://haskell-effectful.github.io+[tests]: https://github.com/haskell-effectful/cache-effectful/blob/main/cache-effectful/test/Main.hs
+ cache-effectful.cabal view
@@ -0,0 +1,84 @@+cabal-version: 3.0+name: cache-effectful+version: 0.0.1.0+synopsis: A Cache effect for the effectful ecosystem.+category: cache+homepage:+ https://github.com/haskell-effectful/cache-effectful/tree/main/cache-effectful#readme++bug-reports: https://github.com/haskell-effectful/cache-effectful/issues+author: Hécate Moonlight+maintainer: Hécate Moonlight+license: MIT+build-type: Simple+tested-with: ghc ==9.10.3 || ==9.12.4 || ==9.14.1+extra-source-files:+ LICENSE.md+ README.md++extra-doc-files:+ CHANGELOG.md++source-repository head+ type: git+ location: https://github.com/haskell-effectful/cache-effectful++common common-extensions+ default-extensions:+ ConstraintKinds+ DataKinds+ FlexibleContexts+ FlexibleInstances+ GADTs+ KindSignatures+ ScopedTypeVariables+ TypeApplications+ TypeOperators++ default-language: Haskell2010++common common-ghc-options+ ghc-options:+ -Wall+ -Wcompat+ -Widentities+ -Wincomplete-record-updates+ -Wincomplete-uni-patterns+ -Wpartial-fields+ -Wredundant-constraints+ -fhide-source-paths+ -Wno-unused-do-bind++common common-rts-options+ ghc-options:+ -rtsopts+ -threaded+ -with-rtsopts=-N++library+ import: common-extensions+ import: common-ghc-options+ hs-source-dirs: src+ exposed-modules: Effectful.Cache+ build-depends:+ base <4.23,+ cache >=0.1.3 && <0.2,+ effectful-core >=2.6 && <2.7,+ hashable >=1.5.1 && <1.6,++test-suite cache-effectful-test+ import: common-extensions+ import: common-ghc-options+ import: common-rts-options+ type: exitcode-stdio-1.0+ main-is: Main.hs+ other-modules: Utils+ hs-source-dirs: test+ build-depends:+ base,+ cache,+ cache-effectful,+ effectful-core,+ hashable,+ tasty >=1.5.4 && <1.6,+ tasty-hunit >=0.10.2 && <0.11,
+ src/Effectful/Cache.hs view
@@ -0,0 +1,160 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE Strict #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : Effectful.Cache+-- Copyright : © Hécate Moonlight, 2021+-- License : MIT+-- Maintainer : hecate@glitchbra.in+-- Stability : stable+--+-- An effect wrapper around 'Data.Cache' for the Effectful ecosystem+module Effectful.Cache+ ( -- * The /Cache/ effect+ Cache (..)++ -- * Handlers+ , runCacheIO++ -- * Cache operations+ , insert+ , lookup+ , keys+ , delete+ , filterWithKey+ ) where++import Control.Monad.IO.Class+import qualified Data.Cache as C+import Data.Hashable+import Data.Kind+import Effectful+import Effectful.Dispatch.Dynamic (interpret, send)+import Prelude hiding (lookup)++-- | Operations on a cache+--+-- Since it is an effect with type variables, you will have the duty of making unambiguous calls to the provided+-- functions.+-- This means that with numerical literals ('3', '4', etc),+-- [visible type applications](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/type_applications.html)+-- will be necessary. See each function's documentation for examples.+--+-- @since 0.0.1.0+data Cache k v :: Effect where+ Insert :: (Eq k, Hashable k) => k -> v -> Cache k v m ()+ Lookup :: (Eq k, Hashable k) => k -> Cache k v m (Maybe v)+ Keys :: Cache k v m [k]+ Delete :: (Eq k, Hashable k) => k -> Cache k v m ()+ FilterWithKey :: (Eq k, Hashable k) => (k -> v -> Bool) -> Cache k v m ()++-- |+-- @since 0.0.1.0+type instance DispatchOf (Cache k v) = 'Dynamic++-- | The default IO handler+--+-- @since 0.0.1.0+runCacheIO+ :: forall (k :: Type) (v :: Type) (es :: [Effect]) (a :: Type)+ . (Hashable k, IOE :> es)+ => C.Cache k v+ -> Eff (Cache k v : es) a+ -> Eff es a+runCacheIO cache = interpret $ \_ -> \case+ Insert key value -> liftIO $ C.insert cache key value+ Lookup key -> liftIO $ C.lookup cache key+ Keys -> liftIO $ C.keys cache+ Delete key -> liftIO $ C.delete cache key+ FilterWithKey fun -> liftIO $ C.filterWithKey fun cache++-- | Insert an item in the cache, using the default expiration value of the cache.+--+-- @since 0.0.1.0+insert+ :: forall (k :: Type) (v :: Type) (es :: [Effect])+ . (Hashable k, Cache k v :> es)+ => k+ -> v+ -> Eff es ()+insert key value = send $ Insert key value++-- | Lookup an item with the given key, and delete it if it is expired.+--+-- The function will only return a value if it is present in the cache and if the item is not expired.+-- The function will eagerly delete the item from the cache if it is expired.+--+-- @since 0.0.1.0+lookup+ :: forall (k :: Type) (v :: Type) (es :: [Effect])+ . (Hashable k, Cache k v :> es)+ => k+ -> Eff es (Maybe v)+lookup key = send $ Lookup key++-- | List all the keys of the cache.+--+-- Since 'Cache' has type variables, you will need to use visible type applications or explicitly typed+-- arguments for the key *and* value parameters to distinguish this 'Cache' from potentially other ones.+--+-- === __Example__+--+-- > listKeys :: (Cache Int Int :> es) => Eff es [Int]+-- > listKeys = do+-- > mapM_ (\(k,v) -> insert @Int @Int k v) [(2,4),(3,6),(4,8),(5,10)]+-- > keys @Int @Int -- [2,3,4,5]+--+-- @since 0.0.1.0+keys+ :: forall (k :: Type) (v :: Type) (es :: [Effect])+ . Cache k v :> es+ => Eff es [k]+keys = send @(Cache k v) Keys++-- | Delete the provided key from the cache it is present.+--+-- Since 'Cache' has type variables, you will need to use visible type applications or explicitly typed+-- arguments for the key *and* value parameters to distinguish this 'Cache' from potentially other ones.+--+-- === __Example__+--+-- > deleteKeys :: (Cache Int Int :> es) => Eff es [Int]+-- > deleteKeys = do+-- > mapM_ (\(k,v) -> insert @Int @Int k v) [(2,4),(3,6),(4,8),(5,10)]+-- > delete @Int @Int 3+-- > delete @Int @Int 5+-- > keys @Int @Int -- [2,4]+--+-- @since 0.0.1.0+delete+ :: forall (k :: Type) (v :: Type) (es :: [Effect])+ . (Hashable k, Cache k v :> es)+ => k+ -> Eff es ()+delete key = send @(Cache k v) $ Delete key++-- | Keeps elements that satisfy the predicate (used for cache invalidation).+--+-- Note that the predicate might be called for expired items.+--+-- Since 'Cache' has type variables, you will need to use visible type applications or explicitly typed+-- arguments for the key *and* value parameters to distinguish this 'Cache' from potentially other ones.+--+-- === __Example__+--+-- > filterKeys :: (Cache Int Int :> es) => Eff es [Int]+-- > filterKeys = do+-- > mapM_ (\(k,v) -> insert @Int @Int k v) [(2,4),(3,6),(4,8),(5,10)]+-- > filterWithKey @Int @Int (\k _ -> k /= 3)+-- > keys @Int @Int -- [2,4,5]+--+-- @since 0.0.1.0+filterWithKey+ :: forall (k :: Type) (v :: Type) (es :: [Effect])+ . (Hashable k, Cache k v :> es)+ => (k -> v -> Bool)+ -> Eff es ()+filterWithKey fun = send $ FilterWithKey fun
+ test/Main.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE NoOverloadedStrings #-}++module Main where++import qualified Data.Cache as C+import Effectful+import Test.Tasty+import Test.Tasty.HUnit+import Prelude hiding (lookup)++import Effectful.Cache+import qualified Utils as U++main :: IO ()+main =+ defaultMain $+ testGroup+ "cache-effectful"+ [ testCase "Insert & Lookup" $ testInsertAndLookup =<< initIntCache+ , testCase "Listing keys" $ testListKeys =<< initIntCache+ , testCase "Deleting keys" $ testDeleteKeys =<< initIntCache+ , testCase "Filter with key" $ testFilterWithKey =<< initIntCache+ ]++initStringCache :: IO (C.Cache String String)+initStringCache = C.newCache Nothing++initIntCache :: IO (C.Cache Int Int)+initIntCache = C.newCache Nothing++populateIntCache :: Cache Int Int :> es => Eff es ()+populateIntCache =+ mapM_ (\(k, v) -> insert @Int @Int k v) [(2, 4), (3, 6), (4, 8), (5, 10)]++---++testInsertAndLookup :: C.Cache Int Int -> Assertion+testInsertAndLookup cache = runEff $ do+ result <- runCacheIO cache insertAndLookup+ U.assertEqual "Looking up key 3 yields the value 12" (Just 12) result+ where+ insertAndLookup :: Cache Int Int :> es => Eff es (Maybe Int)+ insertAndLookup = do+ insert @Int @Int 3 12+ lookup @Int 3++testListKeys :: C.Cache Int Int -> Assertion+testListKeys cache = runEff $ do+ result <- runCacheIO cache listKeys+ U.assertEqual "Correct list of key in the cache" [2, 3, 4, 5] result+ where+ listKeys :: Cache Int Int :> es => Eff es [Int]+ listKeys = do+ populateIntCache+ keys @Int @Int++testDeleteKeys :: C.Cache Int Int -> Assertion+testDeleteKeys cache = runEff $ do+ result <- runCacheIO cache deleteKeys+ U.assertEqual "Keys are deleted" [2, 4] result+ where+ deleteKeys :: Cache Int Int :> es => Eff es [Int]+ deleteKeys = do+ populateIntCache+ delete @Int @Int 3+ delete @Int @Int 5+ keys @Int @Int++testFilterWithKey :: C.Cache Int Int -> Assertion+testFilterWithKey cache = runEff $ do+ result <- runCacheIO cache filterKeys+ U.assertEqual "Keys are properly filtered" [2, 4, 5] result+ where+ filterKeys :: Cache Int Int :> es => Eff es [Int]+ filterKeys = do+ populateIntCache+ filterWithKey @Int @Int (\k _ -> k /= 3)+ keys @Int @Int -- [2,4,5]
+ test/Utils.hs view
@@ -0,0 +1,23 @@+module Utils+ ( assertBool+ , assertEqual+ , assertFailure+ ) where++import Effectful+import GHC.Stack+import qualified Test.Tasty.HUnit as T++assertBool :: (HasCallStack, IOE :> es) => String -> Bool -> Eff es ()+assertBool msg p = liftIO $ T.assertBool msg p++assertEqual+ :: (HasCallStack, Eq a, Show a, IOE :> es)+ => String+ -> a+ -> a+ -> Eff es ()+assertEqual msg expected given = liftIO $ T.assertEqual msg expected given++assertFailure :: (HasCallStack, IOE :> es) => String -> Eff es a+assertFailure msg = liftIO $ T.assertFailure msg