I want to write a simple framework that deals with persisting entities. The idea is to have an Entity type class and to provide generic persistence operations like
storeEntity :: (Entity a) => a -> IO ()
retrieveEntity :: (Entity a) => Integer -> IO a
publishEntity :: (Entity a) => a -> IO ()
Actual data types are instance of that Entity type class.
Even though the persistence operations are generic and don't need any information about the concrete data types You have to provide a type annotation at the call site to make GHC happy, like in:
main = do
let user1 = User 1 "Thomas" "Meier" "[email protected]"
storeEntity user1
user2 <- retrieveEntity 1 :: IO User -- how to avoid this type annotation?
publishEntity user2
Is there any way to avoid this kind of call site annotations?
I know that I don't need these annotations if the compiler can deduce the actual type from the context of the usage. So for example the following code works fine:
main = do
let user1 = User 1 "Thomas" "Meier" "[email protected]"
storeEntity user1
user2 <- retrieveEntity 1
if user1 == user2
then publishEntity user2
else fail "retrieve of data failed"
But I would like to be able to chain the polymorphic actions like so:
main = do
let user1 = User 1 "Heinz" "Meier" "[email protected]"
storeEntity user1
-- unfortunately the next line does not compile
retrieveEntity 1 >>= publishEntity
-- but with a type annotation it works:
(retrieveEntity 1 :: IO User) >>= publishEntity
But having a type annotation here breaks the elegance of the polymorphism...
For completeness sake I've included the full source code:
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module Example where
import GHC.Generics
import Data.Aeson
-- | Entity type class
class (ToJSON e, FromJSON e, Eq e, Show e) => Entity e where
getId :: e -> Integer
-- | a user entity
data User = User {
userId :: Integer
, firstName :: String
, lastName :: String
, email :: String
} deriving (Show, Eq, Generic, ToJSON, FromJSON)
instance Entity User where
getId = userId
-- | load persistent entity of type a and identified by id
retrieveEntity :: (Entity a) => Integer -> IO a
retrieveEntity id = do
-- compute file path based on id
let jsonFileName = getPath id
-- parse entity from JSON file
eitherEntity <- eitherDecodeFileStrict jsonFileName
case eitherEntity of
Left msg -> fail msg
Right e -> return e
-- | store persistent entity of type a to a json file
storeEntity :: (Entity a) => a -> IO ()
storeEntity entity = do
-- compute file path based on entity id
let jsonFileName = getPath (getId entity)
-- serialize entity as JSON and write to file
encodeFile jsonFileName entity
-- | compute path of data file based on id
getPath :: Integer -> String
getPath id = ".stack-work/" ++ show id ++ ".json"
publishEntity :: (Entity a) => a -> IO ()
publishEntity = print
main = do
let user1 = User 1 "Thomas" "Meier" "[email protected]"
storeEntity user1
user2 <- retrieveEntity 1 :: IO User
print user2
Users, e.g., or pattern match on it. I think this will be much less of a problem in real-world usage than it is in toy examples like this one where the only consumer is also polymorphic. - Daniel WagnerretrieveEntity 1 >>= publishEntity @Userbetter (using theTypeApplicationsextension)? You cannot get around the fact that you need to specify the required type somewhere. In this example, the compiler can infer thatretrieveEntityhas to returnIO Userif the monomorphic functionpublishEntity @Useris to receive an appropriate argument. - chepner