Request-local storage #268

Closed
luite opened this Issue Feb 13, 2012 · 42 comments

Comments

Projects
None yet
4 participants
@luite
Member

luite commented Feb 13, 2012

It would be nice to have an easy way to store/cache temporary data for the duration of a single request. For example maybeAuth needs to query the database to get the current user, but we might need the user data at various places in our handler. For this situation it would be useful if we could implement a cachedMaybeAuth like this:

cachedMaybeAuth = do
   c <- lookupMaybeAuthInCache
   case c of
      Just u -> return u
      Nothing -> do
          u <- maybeAuth
          storeInCache u
          return u

But how do we store the cache? The WAI request has a vault, but it can't be modified. Is there another way to do it with the current API, should we add a mutable vault or something similar (if so, where do we store the keys, user keys in foundation type?), other options, ideas?

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Feb 13, 2012

Member

This is an interesting idea! I think we could borrow some ideas from acid-state in order to create a simplified, per-request, memory-only storage.

I mention acid-state since I would like to write something like the following:

data MaybeAuth = MaybeAuth (Maybe (Entity User)) deriving (Typeable)

instance Cached MaybeAuth where
  calculate = MaybeAuth <$> maybeAuth

And then, on user code:

handler = do
  MaybeAuth mauth <- cachedGet

So MaybeAuth would be treated more or less like a transaction is treated in acid-state. Internally, we would maintain a Data.Map.Map TypeRep Dynamic.

Member

meteficha commented Feb 13, 2012

This is an interesting idea! I think we could borrow some ideas from acid-state in order to create a simplified, per-request, memory-only storage.

I mention acid-state since I would like to write something like the following:

data MaybeAuth = MaybeAuth (Maybe (Entity User)) deriving (Typeable)

instance Cached MaybeAuth where
  calculate = MaybeAuth <$> maybeAuth

And then, on user code:

handler = do
  MaybeAuth mauth <- cachedGet

So MaybeAuth would be treated more or less like a transaction is treated in acid-state. Internally, we would maintain a Data.Map.Map TypeRep Dynamic.

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 13, 2012

Member

Ah that does have a simpler API than Vault, no messing with Keys. vault was created to remove the Typeable requirement, but I don't think that's really a problem here. Storing multiple things of the same type would require multiple data types and instances though. Maybe some TH can come in handy here.

Member

luite commented Feb 13, 2012

Ah that does have a simpler API than Vault, no messing with Keys. vault was created to remove the Typeable requirement, but I don't think that's really a problem here. Storing multiple things of the same type would require multiple data types and instances though. Maybe some TH can come in handy here.

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Feb 13, 2012

Member

Well, some tagging has to be done. Typeable is the only type-safe way that I know which is general enough to allow anyone put anything there.

With some TH we could write something like

maybeAuth :: Yesod master => GHandler sub master (Maybe (Entity User))
maybeAuth = maybeAuth -- please bear with me =)
cached 'maybeAuth

Note the cached bit where TH kicks in. User code would be the same.

Member

meteficha commented Feb 13, 2012

Well, some tagging has to be done. Typeable is the only type-safe way that I know which is general enough to allow anyone put anything there.

With some TH we could write something like

maybeAuth :: Yesod master => GHandler sub master (Maybe (Entity User))
maybeAuth = maybeAuth -- please bear with me =)
cached 'maybeAuth

Note the cached bit where TH kicks in. User code would be the same.

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 13, 2012

Member

vault uses tagging with data ( Data.Unique ) rather than with types, though with type-tagged keys to keep things typesafe, and some IORef trick to get rid of Typeable:
http://apfelmus.nfshost.com/blog/2011/09/04-vault.html
(But I think this will go wrong if you use one Key in multiple vaults, so it looks rather useless here, and managing Key data looks more cumbersome than defining some types/instances)
James cook in the comments mention dependent-map, but I have no idea if that might be useful here

Member

luite commented Feb 13, 2012

vault uses tagging with data ( Data.Unique ) rather than with types, though with type-tagged keys to keep things typesafe, and some IORef trick to get rid of Typeable:
http://apfelmus.nfshost.com/blog/2011/09/04-vault.html
(But I think this will go wrong if you use one Key in multiple vaults, so it looks rather useless here, and managing Key data looks more cumbersome than defining some types/instances)
James cook in the comments mention dependent-map, but I have no idea if that might be useful here

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Feb 13, 2012

Member

That's why I said that Typeable is the only way that I know. What's the Key for maybeAuth? There's a tag for that =).

Member

meteficha commented Feb 13, 2012

That's why I said that Typeable is the only way that I know. What's the Key for maybeAuth? There's a tag for that =).

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 13, 2012

Member

oh it looks like the actual implementation of vault on github has a different Key type: newtype Key s a = Key Int, and it's basically just an IntMap with some unsafeCoerce and GHC.Exts.Any

Problem would still be how to generate the keys of course, maybe it would work with some unsafePerformIO and NoInline to be able to do something like

cachedAuthKey :: Key (Maybe (Entity User))
cachedAuthKey = newKey

https://github.com/HeinrichApfelmus/vault/blob/master/src/Data/Vault/ST.hs

Member

luite commented Feb 13, 2012

oh it looks like the actual implementation of vault on github has a different Key type: newtype Key s a = Key Int, and it's basically just an IntMap with some unsafeCoerce and GHC.Exts.Any

Problem would still be how to generate the keys of course, maybe it would work with some unsafePerformIO and NoInline to be able to do something like

cachedAuthKey :: Key (Maybe (Entity User))
cachedAuthKey = newKey

https://github.com/HeinrichApfelmus/vault/blob/master/src/Data/Vault/ST.hs

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Feb 13, 2012

Member

Well, we'd change

AuthKey mauth <- cached

into

mauth <- get cachedAuthKey

=)

Member

meteficha commented Feb 13, 2012

Well, we'd change

AuthKey mauth <- cached

into

mauth <- get cachedAuthKey

=)

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 13, 2012

Member

yeah it could be just as simple to use, I'm just wondering whether we might run into trouble with generating "pure" globally unique integers somehow.

Member

luite commented Feb 13, 2012

yeah it could be just as simple to use, I'm just wondering whether we might run into trouble with generating "pure" globally unique integers somehow.

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 13, 2012

Member

hmm, an actual cachedAuthKey would look more like this:

newKey :: Monad m => m a -> Key m a

cachedMaybeUser :: Key Handler (Maybe User)
cachedMaybeUser = newKey maybeUser
Member

luite commented Feb 13, 2012

hmm, an actual cachedAuthKey would look more like this:

newKey :: Monad m => m a -> Key m a

cachedMaybeUser :: Key Handler (Maybe User)
cachedMaybeUser = newKey maybeUser
@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Feb 13, 2012

Member

That's why I reiterate that I like the Typeable solution since it's guaranteed that TypeReps are unique (under the assumption that you always use DeriveDataTypeable).

Member

meteficha commented Feb 13, 2012

That's why I reiterate that I like the Typeable solution since it's guaranteed that TypeReps are unique (under the assumption that you always use DeriveDataTypeable).

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 14, 2012

Member

Another option for generating unique keys: TH, some unique identifier based on random numbers, current time, whatever, or use TH to splice in a NOINLINE pragma (I'd prefer the first though, let TH generate unique identifiers)

Member

luite commented Feb 14, 2012

Another option for generating unique keys: TH, some unique identifier based on random numbers, current time, whatever, or use TH to splice in a NOINLINE pragma (I'd prefer the first though, let TH generate unique identifiers)

@gregwebs

This comment has been minimized.

Show comment
Hide comment
@gregwebs

gregwebs Feb 14, 2012

Member

It looks like we are coming up with a good general-purpose caching solution that could be used outside of just the basic request cycle. Otherwise I would say lets provide an abstraction that is a mutable variable that gets automatically cleared by ResourceT.

Member

gregwebs commented Feb 14, 2012

It looks like we are coming up with a good general-purpose caching solution that could be used outside of just the basic request cycle. Otherwise I would say lets provide an abstraction that is a mutable variable that gets automatically cleared by ResourceT.

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 14, 2012

Member

A simple toy implementation of how a cache with uuid keys might look:

{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
module Cache (Cache, Key, key, cached, empty) where

import           Control.Applicative ((<$>))
import           Control.Monad.State.Class

import qualified Data.Map as Map
import           Data.Map (Map)
import           Data.UUID (UUID, toWords, fromWords)
import           Data.Word (Word32)

import           GHC.Exts (Any)

import           Language.Haskell.TH.Syntax
import           Language.Haskell.TH.Lib

import           Unsafe.Coerce (unsafeCoerce)

import           System.Random (randomIO)

data Cache = Cache (Map UUID Any)

data Key m a = Key UUID (m a)

instance Eq (Key m a) where (Key u1 _) == (Key u2 _) = u1 == u2
instance Ord (Key m a) where compare (Key u1 _) (Key u2 _) = compare u1 u2

empty :: Cache
empty = Cache Map.empty

cached :: MonadState Cache m => Key m a -> m a
cached (Key u a) = do
  (Cache c) <- get
  case Map.lookup u c of
    Just x  -> return (unsafeCoerce x)
    Nothing -> do
      x <- a
      put (Cache $ Map.insert u (unsafeCoerce x) c)
      return x

key :: Q Exp
key = do
  u <- qRunIO randomIO
  [|\a -> Key $(ue u) a |]

ue :: UUID -> Q Exp
ue u = let (w1,w2,w3,w4) = toWords u
       in [| fromWords |] `appE`
            (wl w1) `appE` (wl w2) `appE` (wl w3) `appE` (wl w4) 

wl :: Word32 -> Q Exp
wl = return . LitE . IntegerL . fromIntegral

Use like this:

{-# LANGUAGE TemplateHaskell #-}

module Test where

import Cache
import Control.Monad.State.Lazy

main = runStateT (replicateM 2 $ cached ktwo >>= liftIO.print) empty

two :: (MonadIO m) => m Int
two = liftIO (putStrLn "generating two") >> return 2

ktwo = $(key) two

gets rid of the Typeable constraint and is 'lighter', compared to an approach based on TypeRep keys, no extra types used, but is it better? any problems to be expected with polymorphic values?

Member

luite commented Feb 14, 2012

A simple toy implementation of how a cache with uuid keys might look:

{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
module Cache (Cache, Key, key, cached, empty) where

import           Control.Applicative ((<$>))
import           Control.Monad.State.Class

import qualified Data.Map as Map
import           Data.Map (Map)
import           Data.UUID (UUID, toWords, fromWords)
import           Data.Word (Word32)

import           GHC.Exts (Any)

import           Language.Haskell.TH.Syntax
import           Language.Haskell.TH.Lib

import           Unsafe.Coerce (unsafeCoerce)

import           System.Random (randomIO)

data Cache = Cache (Map UUID Any)

data Key m a = Key UUID (m a)

instance Eq (Key m a) where (Key u1 _) == (Key u2 _) = u1 == u2
instance Ord (Key m a) where compare (Key u1 _) (Key u2 _) = compare u1 u2

empty :: Cache
empty = Cache Map.empty

cached :: MonadState Cache m => Key m a -> m a
cached (Key u a) = do
  (Cache c) <- get
  case Map.lookup u c of
    Just x  -> return (unsafeCoerce x)
    Nothing -> do
      x <- a
      put (Cache $ Map.insert u (unsafeCoerce x) c)
      return x

key :: Q Exp
key = do
  u <- qRunIO randomIO
  [|\a -> Key $(ue u) a |]

ue :: UUID -> Q Exp
ue u = let (w1,w2,w3,w4) = toWords u
       in [| fromWords |] `appE`
            (wl w1) `appE` (wl w2) `appE` (wl w3) `appE` (wl w4) 

wl :: Word32 -> Q Exp
wl = return . LitE . IntegerL . fromIntegral

Use like this:

{-# LANGUAGE TemplateHaskell #-}

module Test where

import Cache
import Control.Monad.State.Lazy

main = runStateT (replicateM 2 $ cached ktwo >>= liftIO.print) empty

two :: (MonadIO m) => m Int
two = liftIO (putStrLn "generating two") >> return 2

ktwo = $(key) two

gets rid of the Typeable constraint and is 'lighter', compared to an approach based on TypeRep keys, no extra types used, but is it better? any problems to be expected with polymorphic values?

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 14, 2012

Member

Ok this doesn't work of course, since you can make multiple types with the same key:

main :: IO ()
main = do
  flip runStateT empty $ do
    (t1 :: Double) <- cached kthree
    (t2 :: Int) <- cached kthree
    liftIO $ putStrLn (show t1 ++ " " ++ show t2)
  return ()

three :: (Num a, MonadIO m) => m a
three = liftIO (putStrLn "generating three") >> return 3

kthree :: (Num a, MonadIO m) => Key m a
kthree = $(key) three
Member

luite commented Feb 14, 2012

Ok this doesn't work of course, since you can make multiple types with the same key:

main :: IO ()
main = do
  flip runStateT empty $ do
    (t1 :: Double) <- cached kthree
    (t2 :: Int) <- cached kthree
    liftIO $ putStrLn (show t1 ++ " " ++ show t2)
  return ()

three :: (Num a, MonadIO m) => m a
three = liftIO (putStrLn "generating three") >> return 3

kthree :: (Num a, MonadIO m) => Key m a
kthree = $(key) three
@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Feb 15, 2012

Member

Hmm, looks like Yesod.Internal.Cache already uses this approach, with the same problem, and the GHState type has one, so the question is whether it should be changed to some safer scheme to make it more suitable for general use by end users (tagging by Typeable probably), and promote it to a non-internal module?

Member

luite commented Feb 15, 2012

Hmm, looks like Yesod.Internal.Cache already uses this approach, with the same problem, and the GHState type has one, so the question is whether it should be changed to some safer scheme to make it more suitable for general use by end users (tagging by Typeable probably), and promote it to a non-internal module?

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Mar 12, 2012

Member

Sorry I'm coming late to this, but I don't understand what the problem is with using vault. It seems like the write solution here.

Member

snoyberg commented Mar 12, 2012

Sorry I'm coming late to this, but I don't understand what the problem is with using vault. It seems like the write solution here.

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Mar 12, 2012

Member

The problem with vault is that you don't have top-level global keys, you run an IO action to make a new key, and you still need some way to store your keys.

Member

luite commented Mar 12, 2012

The problem with vault is that you don't have top-level global keys, you run an IO action to make a new key, and you still need some way to store your keys.

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Mar 12, 2012

Member

I see two solutions to this:

  • Use TH
  • Use unsafePerformIO
Member

snoyberg commented Mar 12, 2012

I see two solutions to this:

  • Use TH
  • Use unsafePerformIO
@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Mar 12, 2012

Member

Yes, but TH isn't really safe if you have polymorphic keys. A user could unknowingly unsafeCoerce his cached value by forgetting a type sig for his Key.

I forgot what the problem with unsafePerformIO was, probably something with users having to write NOINLINE pragma's to prevent actions from running multiple times.

Member

luite commented Mar 12, 2012

Yes, but TH isn't really safe if you have polymorphic keys. A user could unknowingly unsafeCoerce his cached value by forgetting a type sig for his Key.

I forgot what the problem with unsafePerformIO was, probably something with users having to write NOINLINE pragma's to prevent actions from running multiple times.

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

@luite Could you elaborate on the TH issue you've described? I've pictured on my mind something like

data K t = K t deriving (Typeable)

and then you put K Int on the cache and take back a K String, is that what you meant? I don't think this is an issue, because the TypeReps would be different. For example:

Prelude Data.Typeable> typeOf (Just True)
Maybe Bool
Prelude Data.Typeable> typeOf (Just ())
Maybe ()
Member

meteficha commented Mar 12, 2012

@luite Could you elaborate on the TH issue you've described? I've pictured on my mind something like

data K t = K t deriving (Typeable)

and then you put K Int on the cache and take back a K String, is that what you meant? I don't think this is an issue, because the TypeReps would be different. For example:

Prelude Data.Typeable> typeOf (Just True)
Maybe Bool
Prelude Data.Typeable> typeOf (Just ())
Maybe ()
@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Mar 12, 2012

Member

Yes, the NOINLINE stuff could be a problem, though I believe in practice it would mostly work without it. As for TH, we would still require explicit types:

mykey :: Key Foo
mykey = $(generateKey)
Member

snoyberg commented Mar 12, 2012

Yes, the NOINLINE stuff could be a problem, though I believe in practice it would mostly work without it. As for TH, we would still require explicit types:

mykey :: Key Foo
mykey = $(generateKey)
@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

@luite Ok, sorry, I've messed the terms on my mind. I read "TH" as "Typeable". So, Michael, there's another solution, which is the one I like the most: #268 (comment).

Member

meteficha commented Mar 12, 2012

@luite Ok, sorry, I've messed the terms on my mind. I read "TH" as "Typeable". So, Michael, there's another solution, which is the one I like the most: #268 (comment).

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Mar 12, 2012

Member

is it possible to check or enforce that all $(generateKey) things have a monomorphic type? If not, I'd not be very happy to have that as a unsafeCoerce-in-disguise API exposed to users. I do like @meteficha's suggestion, but its obvious drawback is that you need a Typeable constraint for everything you need to store.

Member

luite commented Mar 12, 2012

is it possible to check or enforce that all $(generateKey) things have a monomorphic type? If not, I'd not be very happy to have that as a unsafeCoerce-in-disguise API exposed to users. I do like @meteficha's suggestion, but its obvious drawback is that you need a Typeable constraint for everything you need to store.

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

@luite The idea isn't having Typeable on everything, otherwise e.g. you'd able to store just one cached value of type Text. The idea is using a newtype for each value you want cached, and deriving Typeable just for the newtype.

Member

meteficha commented Mar 12, 2012

@luite The idea isn't having Typeable on everything, otherwise e.g. you'd able to store just one cached value of type Text. The idea is using a newtype for each value you want cached, and deriving Typeable just for the newtype.

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

I'd love to have this feature on Yesod 1.0. I still think that the Typeable route that I've described is the best one for the following reasons:

  1. Completely type-safe (supposing that you always deriving Typeable). You can't screw it, it's prevented by the type system.
  2. Readable code when getting the cached value, just MyValue val <- cached and you're good to go.
  3. Not beautiful, but manageable code when creating cached types (newtype and instance Cacheable).
  4. No need for TH.

Like I said, I think that using Typeable is the only completely type-safe way of doing something like this without needing to carry keys around as with Vault -- in essence, the TypeRep is the key that the compiler carries around for you via type classes.

So I'd like to decide which route should we take. I may implement my idea in the new couple of days to see how it goes.

Member

meteficha commented Mar 12, 2012

I'd love to have this feature on Yesod 1.0. I still think that the Typeable route that I've described is the best one for the following reasons:

  1. Completely type-safe (supposing that you always deriving Typeable). You can't screw it, it's prevented by the type system.
  2. Readable code when getting the cached value, just MyValue val <- cached and you're good to go.
  3. Not beautiful, but manageable code when creating cached types (newtype and instance Cacheable).
  4. No need for TH.

Like I said, I think that using Typeable is the only completely type-safe way of doing something like this without needing to carry keys around as with Vault -- in essence, the TypeRep is the key that the compiler carries around for you via type classes.

So I'd like to decide which route should we take. I may implement my idea in the new couple of days to see how it goes.

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Mar 12, 2012

Member

I'd personally wouldn't just want a Cacheable type for the cache, since it isn't always possible to implement the calculate method without access to things like function parameters or user-specific monad stacks. Perhaps a plain get/put cache with convenience functions like:

MyValue val <- cachedGet actionToMakeVal
Member

luite commented Mar 12, 2012

I'd personally wouldn't just want a Cacheable type for the cache, since it isn't always possible to implement the calculate method without access to things like function parameters or user-specific monad stacks. Perhaps a plain get/put cache with convenience functions like:

MyValue val <- cachedGet actionToMakeVal
@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

@luite The problem of caching with arguments is: what if the arguments change? Should it recache? Should it just ignore it? With cachedGet actionToMakeVal, it'll just ignore the action and give you an incorrect value.

But, here's an idea. How about having this on Yesod:

class Cached m a where
  calculate :: m a

cached :: Cached (GHandler sub master) a => GHandler sub master a
cached = cachedT id

cachedT :: Cached m a => (forall x. GHandler sub master x -> m x) -> m a

This solves the custom monad stack problem by using cachedT lift (or cachedT (lift . lift)...). I'm not 100% sure that cachedT may be implemented the way I wrote it above, but I think it is possible.

This also solves the argument problem, since your monad transformer may just be ReaderT. Which means that whenever you use cachedT, you're on your own in the sense that it's your responsibility to ensure that the lifting argument is always correct and makes sense.

What do you think?

Member

meteficha commented Mar 12, 2012

@luite The problem of caching with arguments is: what if the arguments change? Should it recache? Should it just ignore it? With cachedGet actionToMakeVal, it'll just ignore the action and give you an incorrect value.

But, here's an idea. How about having this on Yesod:

class Cached m a where
  calculate :: m a

cached :: Cached (GHandler sub master) a => GHandler sub master a
cached = cachedT id

cachedT :: Cached m a => (forall x. GHandler sub master x -> m x) -> m a

This solves the custom monad stack problem by using cachedT lift (or cachedT (lift . lift)...). I'm not 100% sure that cachedT may be implemented the way I wrote it above, but I think it is possible.

This also solves the argument problem, since your monad transformer may just be ReaderT. Which means that whenever you use cachedT, you're on your own in the sense that it's your responsibility to ensure that the lifting argument is always correct and makes sense.

What do you think?

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Mar 12, 2012

Member

I think that it's relatively clear to have a cachedGet action with the semantics "give me the cached value, if you don't have one run the action to create one". Since you can't compare actions in general, and there is no Eq constraint on the cached values, the types make it clear that this is actually the only sensible thing it can do.

I think that a cache should at least have a get/put/delete interface (*), and the still relatively low-level cachedGet. As for the Cached type class, I think it could be convenient, but I guess I'd need to see it in action first :)

(*) I'm thinking of a a use case where you have a cache that has a longer lifetime than a single request, where items from the cache need to be expired in response to certain events, but only recalculated on demand.

Member

luite commented Mar 12, 2012

I think that it's relatively clear to have a cachedGet action with the semantics "give me the cached value, if you don't have one run the action to create one". Since you can't compare actions in general, and there is no Eq constraint on the cached values, the types make it clear that this is actually the only sensible thing it can do.

I think that a cache should at least have a get/put/delete interface (*), and the still relatively low-level cachedGet. As for the Cached type class, I think it could be convenient, but I guess I'd need to see it in action first :)

(*) I'm thinking of a a use case where you have a cache that has a longer lifetime than a single request, where items from the cache need to be expired in response to certain events, but only recalculated on demand.

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

@luite I'm thinking only about request-local storage. Also, global caches are easy to implement using your foundation data type. Something in the middle I really don't know, perhaps an LRU on your foundation.

About get/put/delete: IMHO, we shouldn't have all this power. A request is something that doesn't last for too long, delete doesn't make sense. I prefer to automatically put when getting, so the only use of put would be to overwrite a previous value, which I think is pretty evil and I wouldn't like to support that.

And, instead of supporting a full blown monad stack, another idea is to have explicit support for cached actions that need arguments:

class Typeable a => Cached a where
  type Argument a :: *
  calculate :: Argument a -> GHandler sub master a

cached :: (Cached a, Argument a ~ ()) => GHandler sub master a
cached = cachedWith ()

cachedWith :: (Ord (Argument a), Cached a) => Argument a -> GHandler sub master a
cachedWith = ...

I find this nicer than my previous suggestion since now it's not your responsibility to ensure that your argument is right, since the cached value given will now depend on the argument. I think this is pretty nice, you may have something like

newtype CachedUser = CachedUser User

instance Cached CachedUser where
  type Argument CachedUser = UserId
  calculate = fmap CachedUser . runDB . get

... = do
  CachedUser user1 <- cachedWith uid1
  CachedUser user2 <- cachedWith uid2

and the Users will be fetched from the DB only when needed. If your data type is not an instance of Ord, you could use

newtype NoOrd a = NoOrd a
instance Eq  (NoOrd a) where _ == _ = True
instance Ord (NoOrd a) where compare _ _ = Eq

which brings back the problem of different arguments, but now you're forced to tag it as sort-of-unsafe.

Something I don't like about this approach is shown in my CachedUser example: there were two transactions, and it would be nice to have just one. But I don't know how to solve this problem, since the cache would need to be somehow rolled back in a hypothetical runDB $ do {Foo x <- cached; error "oops"}, so I'll just ignore this issue.

Member

meteficha commented Mar 12, 2012

@luite I'm thinking only about request-local storage. Also, global caches are easy to implement using your foundation data type. Something in the middle I really don't know, perhaps an LRU on your foundation.

About get/put/delete: IMHO, we shouldn't have all this power. A request is something that doesn't last for too long, delete doesn't make sense. I prefer to automatically put when getting, so the only use of put would be to overwrite a previous value, which I think is pretty evil and I wouldn't like to support that.

And, instead of supporting a full blown monad stack, another idea is to have explicit support for cached actions that need arguments:

class Typeable a => Cached a where
  type Argument a :: *
  calculate :: Argument a -> GHandler sub master a

cached :: (Cached a, Argument a ~ ()) => GHandler sub master a
cached = cachedWith ()

cachedWith :: (Ord (Argument a), Cached a) => Argument a -> GHandler sub master a
cachedWith = ...

I find this nicer than my previous suggestion since now it's not your responsibility to ensure that your argument is right, since the cached value given will now depend on the argument. I think this is pretty nice, you may have something like

newtype CachedUser = CachedUser User

instance Cached CachedUser where
  type Argument CachedUser = UserId
  calculate = fmap CachedUser . runDB . get

... = do
  CachedUser user1 <- cachedWith uid1
  CachedUser user2 <- cachedWith uid2

and the Users will be fetched from the DB only when needed. If your data type is not an instance of Ord, you could use

newtype NoOrd a = NoOrd a
instance Eq  (NoOrd a) where _ == _ = True
instance Ord (NoOrd a) where compare _ _ = Eq

which brings back the problem of different arguments, but now you're forced to tag it as sort-of-unsafe.

Something I don't like about this approach is shown in my CachedUser example: there were two transactions, and it would be nice to have just one. But I don't know how to solve this problem, since the cache would need to be somehow rolled back in a hypothetical runDB $ do {Foo x <- cached; error "oops"}, so I'll just ignore this issue.

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

Actually, it's almost possible to have a completely general

newtype CachedEntity backend a = CachedEntity (Maybe a) deriving (Typeable)

instance PersistEntity a => Cached (CachedEntity backend a) where
  type Argument (CachedEntity backend a) = Key backend a
  calculate = fmap CachedEntity . runDB . get

However, we'd need (at least) a constraint YesodPersist master and a constraint YesodPersistBackend master ~ backend, which isn't possible in the design I've presented. Actually, what I've presented is almost useless since your code may not access your foundation data type! Oops!

Here's an updated Cached type class:

class Typeable a => Cached master a where
  type Argument a :: *
  calculate :: Argument a -> GHandler sub master a

cached :: (Cached master a, Argument a ~ ()) => GHandler sub master a
cached = cachedWith ()

cachedWith :: (Ord (Argument a), Cached master a) => Argument a -> GHandler sub master a
cachedWith = ...

and now you may write (untested, of course)

newtype CachedEntity a = CachedEntity (Maybe a) deriving (Typeable)

instance (PersistEntity a, YesodPersist master) => Cached master (CachedEntity a) where
  type Argument (CachedEntity a) = Key (YesodPersistBackend master) a
  calculate = fmap CachedEntity . runDB . get

I think this is pretty awesome!

Member

meteficha commented Mar 12, 2012

Actually, it's almost possible to have a completely general

newtype CachedEntity backend a = CachedEntity (Maybe a) deriving (Typeable)

instance PersistEntity a => Cached (CachedEntity backend a) where
  type Argument (CachedEntity backend a) = Key backend a
  calculate = fmap CachedEntity . runDB . get

However, we'd need (at least) a constraint YesodPersist master and a constraint YesodPersistBackend master ~ backend, which isn't possible in the design I've presented. Actually, what I've presented is almost useless since your code may not access your foundation data type! Oops!

Here's an updated Cached type class:

class Typeable a => Cached master a where
  type Argument a :: *
  calculate :: Argument a -> GHandler sub master a

cached :: (Cached master a, Argument a ~ ()) => GHandler sub master a
cached = cachedWith ()

cachedWith :: (Ord (Argument a), Cached master a) => Argument a -> GHandler sub master a
cachedWith = ...

and now you may write (untested, of course)

newtype CachedEntity a = CachedEntity (Maybe a) deriving (Typeable)

instance (PersistEntity a, YesodPersist master) => Cached master (CachedEntity a) where
  type Argument (CachedEntity a) = Key (YesodPersistBackend master) a
  calculate = fmap CachedEntity . runDB . get

I think this is pretty awesome!

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 12, 2012

Member

(This was meant to be an EDIT but got too long.) Here's an example of CachedEntity:

... = do
  CachedEntity (Just blogPost) <- cachedWith blogPostId
  CachedEntity (Just user) <- cachedWith (BlogPostUserId blogPost)

which has the unfortunate problem of two transactions. But, for example, if you've used maybeAuth in the past, and the current logged in user is the one who posted that blog post, then his User will already be cached.

Member

meteficha commented Mar 12, 2012

(This was meant to be an EDIT but got too long.) Here's an example of CachedEntity:

... = do
  CachedEntity (Just blogPost) <- cachedWith blogPostId
  CachedEntity (Just user) <- cachedWith (BlogPostUserId blogPost)

which has the unfortunate problem of two transactions. But, for example, if you've used maybeAuth in the past, and the current logged in user is the one who posted that blog post, then his User will already be cached.

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Mar 15, 2012

Member

I'm happy with going with this approach, even if I don't think I fully understand it yet. Do either of you want to implement it?

Member

snoyberg commented Mar 15, 2012

I'm happy with going with this approach, even if I don't think I fully understand it yet. Do either of you want to implement it?

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Mar 28, 2012

Member

Any updates on this? Do we have a clear direction to take here?

Member

snoyberg commented Mar 28, 2012

Any updates on this? Do we have a clear direction to take here?

@gregwebs

This comment has been minimized.

Show comment
Hide comment
@gregwebs

gregwebs Mar 31, 2012

Member

hi guys, I haven't paid very close attention to this. Only thing I want to keep in mind is that we should look at using the PersistStore (Key-Value) API for storing things. In process memory doesn't scale well once you have multiple application instances (although we should probably create an in-memory backend that you can start off with). Disk-based database key lookup is pretty fast, but creating a Redis backend for Persist Store should be easy enough also.

This isn't required to start though. First to implement caching decides implementation!

Member

gregwebs commented Mar 31, 2012

hi guys, I haven't paid very close attention to this. Only thing I want to keep in mind is that we should look at using the PersistStore (Key-Value) API for storing things. In process memory doesn't scale well once you have multiple application instances (although we should probably create an in-memory backend that you can start off with). Disk-based database key lookup is pretty fast, but creating a Redis backend for Persist Store should be easy enough also.

This isn't required to start though. First to implement caching decides implementation!

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Mar 31, 2012

Member

@gregwebs I think your comment is relevant to session storage, not request storage. The idea here would be for a short-term storage that only lasts for a single request. Thus, it should stay in memory and we needn't worry about horizontal scaling.

A prime example of a time when this would be useful would be avoiding the need to pull the user information from the database multiple times in different widgets.

Member

snoyberg commented Mar 31, 2012

@gregwebs I think your comment is relevant to session storage, not request storage. The idea here would be for a short-term storage that only lasts for a single request. Thus, it should stay in memory and we needn't worry about horizontal scaling.

A prime example of a time when this would be useful would be avoiding the need to pull the user information from the database multiple times in different widgets.

@gregwebs

This comment has been minimized.

Show comment
Hide comment
@gregwebs

gregwebs Mar 31, 2012

Member

oh, sorry, I am getting into caching across multiple requests (not necessarily per-session).

Member

gregwebs commented Mar 31, 2012

oh, sorry, I am getting into caching across multiple requests (not necessarily per-session).

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Jun 5, 2012

Member

Was there any progress on this? Do we have an idea on a way forward?

Member

snoyberg commented Jun 5, 2012

Was there any progress on this? Do we have an idea on a way forward?

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Jun 13, 2012

Member

Wait a moment, there is per-request caching already! It appears to have landed 7 months ago! How did we all forget about it?! It even has a nice API =).

Anyways, I'd only like to include the following function:

-- | If the 'CacheKey' is already filled, return the cached value.  
-- Otherwise use supplied action to calculate the value and then cache it.
cache??? :: CacheKey a
         -> GHandler sub master a
         -> GHandler sub master a
cache??? key act = do
  mv <- cacheLookup key
  case mv of
    Just v -> return v
    Nothing -> do
      v <- act
      cacheInsert key v
      return v

The name is up to bikeshedding, maybe just cached?

Member

meteficha commented Jun 13, 2012

Wait a moment, there is per-request caching already! It appears to have landed 7 months ago! How did we all forget about it?! It even has a nice API =).

Anyways, I'd only like to include the following function:

-- | If the 'CacheKey' is already filled, return the cached value.  
-- Otherwise use supplied action to calculate the value and then cache it.
cache??? :: CacheKey a
         -> GHandler sub master a
         -> GHandler sub master a
cache??? key act = do
  mv <- cacheLookup key
  case mv of
    Just v -> return v
    Nothing -> do
      v <- act
      cacheInsert key v
      return v

The name is up to bikeshedding, maybe just cached?

@luite

This comment has been minimized.

Show comment
Hide comment
@luite

luite Jun 13, 2012

Member

I didn't know about this when i opened the ticket, but I have mentioned that module in an earlier message. The problem is that keys aren't typesafe, and if you have a polymorphic cache key, you can unknowingly do an unsafeCoerce. As long as it's an Internal module for some speed optimizations, I'm happy with this, but I'd not feel terribly comfortable exposing this as general functionality to end users.

I'm still interested in some better caching api (with Typeable probably), but have been too busy with other things to write an implementation.

Member

luite commented Jun 13, 2012

I didn't know about this when i opened the ticket, but I have mentioned that module in an earlier message. The problem is that keys aren't typesafe, and if you have a polymorphic cache key, you can unknowingly do an unsafeCoerce. As long as it's an Internal module for some speed optimizations, I'm happy with this, but I'd not feel terribly comfortable exposing this as general functionality to end users.

I'm still interested in some better caching api (with Typeable probably), but have been too busy with other things to write an implementation.

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Dec 26, 2012

Member

Do we have any ideas on how to proceed here?

Member

snoyberg commented Dec 26, 2012

Do we have any ideas on how to proceed here?

@snoyberg

This comment has been minimized.

Show comment
Hide comment
@snoyberg

snoyberg Mar 10, 2013

Member

OK, I just implemented a Typeable-based caching in 9559c2a. Can you guys have a look and let me know what you think?

Member

snoyberg commented Mar 10, 2013

OK, I just implemented a Typeable-based caching in 9559c2a. Can you guys have a look and let me know what you think?

@snoyberg snoyberg closed this Mar 18, 2013

@meteficha

This comment has been minimized.

Show comment
Hide comment
@meteficha

meteficha Mar 21, 2013

Member

Beautiful! Looks very nice!

Member

meteficha commented Mar 21, 2013

Beautiful! Looks very nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment