Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cached newtype #15

Merged
merged 3 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Revision history for cached-io

## 1.3.0.0

- **Breaking** Caching functions previously returned `m (t a)`, but it was easy to accidentally use `join` when `m` and `t` were the same monad (eg. `IO (IO a)`), and not get any caching at all. These functions now use a `Cached` newtype for `t a` to make it more difficult to misuse.

### Migrating from <=1.2.0.0 to 1.3.0.0

```haskell
-- Previous versions
f :: IO ()
f = do
cachedAction <- cachedIO action :: IO (IO a)
cachedResult <- cachedAction

-- New version
f :: IO ()
f = do
cachedAction <- cachedIO action :: IO (Cached IO a)
cachedResult <- runCached cachedAction
```

## 1.2.0.0

Thank you [glasserc](https://github.com/glasserc) for your work on previous versions, and a special thanks to
Expand Down
2 changes: 1 addition & 1 deletion cached-io.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.2
name: cached-io
version: 1.2.0.0
version: 1.3.0.0
synopsis: A simple library to cache IO actions
description:
Provides functions that convert an IO action into a cached one by storing the
Expand Down
24 changes: 15 additions & 9 deletions src/Control/Concurrent/CachedIO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
-- > -- Downloads a large payload from an external data store.
Tristano8 marked this conversation as resolved.
Show resolved Hide resolved
-- > downloadData :: IO ByteString
-- >
-- > cachedDownloadData :: IO ByteString
-- > cachedDownloadData :: IO (Cached IO ByteString)
-- > cachedDownloadData = cachedIO (secondsToNominalDiffTime 600) downloadData
--
-- The first time @cachedDownloadData@ is called, it calls @downloadData@,
Expand All @@ -14,6 +14,7 @@
-- result again.
--
module Control.Concurrent.CachedIO (
Cached(..),
cachedIO,
cachedIOWith,
cachedIO',
Expand All @@ -26,6 +27,11 @@ import Control.Monad.Catch (MonadCatch, onException)
import Control.Monad.IO.Class (liftIO, MonadIO)
import Data.Time.Clock (NominalDiffTime, addUTCTime, getCurrentTime, UTCTime)

-- | A cached IO action in some monad @m@. Use 'runCached' to extract the action when you want to query it.
--
-- Note that using 'Control.Monad.join' when the cached action and the outer monad are the same will ignore caching.
newtype Cached m a = Cached {runCached :: m a}

data State a = Uninitialized | Initializing | Updating a | Fresh UTCTime a

-- | Cache an IO action, producing a version of this IO action that is cached
Expand All @@ -37,7 +43,7 @@ data State a = Uninitialized | Initializing | Updating a | Fresh UTCTime a
cachedIO :: (MonadIO m, MonadIO t, MonadCatch t)
=> NominalDiffTime -- ^ Number of seconds before refreshing cache
-> t a -- ^ IO action to cache
-> m (t a)
-> m (Cached t a)
cachedIO interval = cachedIOWith (secondsPassed interval)

-- | Cache an IO action, producing a version of this IO action that is cached
Expand All @@ -50,7 +56,7 @@ cachedIO' :: (MonadIO m, MonadIO t, MonadCatch t)
=> NominalDiffTime -- ^ Number of seconds before refreshing cache
-> (Maybe (UTCTime, a) -> t a) -- ^ action to cache. The stale value and its refresh date
-- are passed so that the action can perform external staleness checks
-> m (t a)
-> m (Cached t a)
cachedIO' interval = cachedIOWith' (secondsPassed interval)

-- | Check if @starting time@ + @seconds@ is after @end time@
Expand All @@ -70,7 +76,7 @@ cachedIOWith
-- If 'isCacheStillFresh' 'lastUpdated' 'now' returns 'True'
-- the cache is considered still fresh and returns the cached IO action
-> t a -- ^ action to cache.
-> m (t a)
-> m (Cached t a)
cachedIOWith f io = cachedIOWith' f (const io)

-- | Cache an IO action, The cache begins uninitialized.
Expand All @@ -84,10 +90,10 @@ cachedIOWith'
-- the cache is considered still fresh and returns the cached IO action
-> (Maybe (UTCTime, a) -> t a) -- ^ action to cache. The stale value and its refresh date
-- are passed so that the action can perform external staleness checks
-> m (t a)
-> m (Cached t a)
cachedIOWith' isCacheStillFresh io = do
cachedT <- liftIO (atomically (newTVar Uninitialized))
return $ do
pure . Cached $ do
now <- liftIO getCurrentTime
join . liftIO . atomically $ do
cached <- readTVar cachedT
Expand All @@ -100,12 +106,12 @@ cachedIOWith' isCacheStillFresh io = do
-- thread will get the stale data instead.
| otherwise -> do
writeTVar cachedT (Updating value)
return $ refreshCache previousState cachedT
pure (refreshCache previousState cachedT)
-- Another thread is already updating the cache, just return the stale value
Updating value -> return (return value)
Updating value -> pure (pure value)
-- The cache is uninitialized. Mark the cache as initializing to block other
-- threads. Initialize and return.
Uninitialized -> return $ refreshCache Uninitialized cachedT
Uninitialized -> pure (refreshCache Uninitialized cachedT)
-- The cache is uninitialized and another thread is already attempting to
-- initialize it. Block.
Initializing -> retry
Expand Down
6 changes: 3 additions & 3 deletions test/test-cachedIO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Main (
main
) where

import Control.Concurrent.CachedIO (cachedIO)
import Control.Concurrent.CachedIO (cachedIO, Cached(..))
import Data.List (isInfixOf)

crawlTheInternet :: IO [String]
Expand All @@ -13,9 +13,9 @@ crawlTheInternet = do
return ["website about Haskell", "website about Ruby", "slashdot.org",
"The Monad.Reader", "haskellwiki"]

searchEngine :: String -> IO [String] -> IO [String]
searchEngine :: String -> Cached IO [String] -> IO [String]
searchEngine query internet = do
pages <- internet
pages <- runCached internet
return $ filter (query `isInfixOf`) pages

main :: IO ()
Expand Down