如何在 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
我编写了一个简单的 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