如何在 Haskell 中进行类型反射

How to have type reflection in Haskell

我编写了一个简单的 Yesod Rest 服务器,它在 JSON 文件中保存实体。 实体存储在磁盘上名为 data/type.id.json 的文件中。 例如 retrieveCustomer "1234" 应该从文件 data/Customer.1234.json.

加载数据

我正在使用多态函数 retrieveEntity,它可以检索实例化 FromJSON 类型类的任何数据类型的实例。 (这部分效果很好)

但目前我必须填写在类型特定函数(如 retrieveCustomer)中硬编码的类型名称。

如何设法在通用 retrieveEntity 中动态计算类型名称? 我想我基本上是在寻找一种 Haskell 类型的反射机制,我到目前为止还没有遇到过?

-- | retrieve a Customer by id
retrieveCustomer :: Text -> IO Customer
retrieveCustomer id = do
    retrieveEntity "Customer" id :: IO Customer

-- | load a persistent entity of type t and identified by id from the backend
retrieveEntity :: (FromJSON a) => String -> Text -> IO a
retrieveEntity t id = do
    let jsonFileName = getPath t id ".json"
    parseFromJsonFile jsonFileName :: FromJSON a => IO a

-- | compute path of data file
getPath :: String -> Text -> String -> String
getPath t id ex = "data/" ++ t ++ "." ++ unpack id ++ ex

-- | read from file fileName and then parse the contents as a FromJSON instance.
parseFromJsonFile :: FromJSON a => FilePath -> IO a
parseFromJsonFile fileName = do
    contentBytes <- B.readFile fileName
    case eitherDecode contentBytes of
        Left msg -> fail msg
        Right x  -> return x

我想标准的技巧是使用 Typeable,特别是 typeOf :: Typeable a => a -> TypeRep。不幸的是,在我们读取文件之前,我们没有一个 a 可以调用它,直到我们拥有正确的文件名,我们才能做到这一点,直到我们调用typeOf,在我们读取文件之前我们不能这样做...

...或者我们可以吗?

{-# LANGUAGE RecursiveDo #-}
import Data.Aeson
import Data.Text
import Data.Typeable
import qualified Data.ByteString.Lazy as B

retrieveEntity :: (FromJSON a, Typeable a) => Text -> IO a
retrieveEntity id = mdo
    let jsonFileName = getPath (typeOf result) id ".json"
    result <- parseFromJsonFile jsonFileName
    return result

getPath :: TypeRep -> Text -> String -> String
getPath tr id ex = "data/" ++ show tr ++ "." ++ unpack id ++ ex

parseFromJsonFile :: FromJSON a => FilePath -> IO a
parseFromJsonFile fileName = do
    contentBytes <- B.readFile fileName
    case eitherDecode contentBytes of
        Left msg -> fail msg
        Right x  -> return x

或者有更少的令人费解的选项,例如使用 typeRep :: Typeable a => proxy a -> TypeRep。然后我们可以使用 ScopedTypeVariables 将适当的类型引入范围。

{-# LANGUAGE ScopedTypeVariables #-}
import Data.Aeson
import Data.Text
import Data.Typeable
import qualified Data.ByteString.Lazy as B

-- don't forget the forall, it's a STV requirement
retrieveEntity :: forall a. (FromJSON a, Typeable a) => Text -> IO a
retrieveEntity id = do
    let jsonFileName = getPath (typeRep ([] :: [a])) id ".json"
    result <- parseFromJsonFile jsonFileName
    return result

getPath :: TypeRep -> Text -> String -> String
getPath tr id ex = "data/" ++ show tr ++ "." ++ unpack id ++ ex

parseFromJsonFile :: FromJSON a => FilePath -> IO a
parseFromJsonFile fileName = do
    contentBytes <- B.readFile fileName
    case eitherDecode contentBytes of
        Left msg -> fail msg
        Right x  -> return x