Haskell如何实现类型反射?Yesod服务动态获取类型名
You’re right that Haskell’s type reflection (via the Typeable type class) is the perfect solution here. This lets us extract the type name dynamically, eliminating the need to hardcode it in each specific retrieve function. Here’s how to adjust your code:
Step 1: Add Required Imports
First, import the Typeable utilities we’ll need to reflect on type names:
import Data.Typeable (Typeable, typeRep, typeRepTyCon, tyConName, Proxy(..)) import Data.Aeson (FromJSON, eitherDecode) import qualified Data.ByteString.Lazy as B import Data.Text (Text, unpack)
Step 2: Simplify Specific Retrieve Functions
Since we’ll get the type name dynamically, we no longer need to pass hardcoded strings like "Customer" to retrieveEntity:
-- | Retrieve a Customer by id retrieveCustomer :: Text -> IO Customer retrieveCustomer = retrieveEntity
This pattern works for any entity type—just define a one-line function that delegates to retrieveEntity.
Step 3: Modify retrieveEntity to Use Type Reflection
Update retrieveEntity to remove the explicit type string parameter. Instead, use Typeable to extract the type name of the target entity:
-- | Load a persistent entity of type t and identified by id from the backend retrieveEntity :: (FromJSON a, Typeable a) => Text -> IO a retrieveEntity entityId = do -- Extract the unqualified type name (e.g., "Customer" for the Customer type) let typeName = tyConName $ typeRepTyCon $ typeRep (Proxy :: Proxy a) jsonFileName = getPath typeName entityId ".json" parseFromJsonFile jsonFileName
Breakdown of the Type Reflection Logic:
typeRep (Proxy :: Proxy a): Gets aTypeRepvalue that represents the generic typea.typeRepTyCon: Extracts the type constructor from theTypeRep(e.g., the constructor for theCustomertype).tyConName: Converts the type constructor to its unqualified string name (exactly what we need for the file path).
Step 4: Keep Existing Helper Functions
Your getPath and parseFromJsonFile functions work perfectly as-is—they just use the dynamically generated type name string:
-- | 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
Key Benefits:
- No more hardcoding: You won’t have to update string literals if you rename a type or add new entities.
- Full generality: Any type that implements both
FromJSONandTypeablecan useretrieveEntitywithout extra code. - Consistent naming: The file naming scheme (
data/Type.id.json) is enforced automatically based on the actual type.
Edge Case Note:
If your types are defined in a module (e.g., MyApp.Models.Customer), tyConName still returns just "Customer" (the unqualified name), which matches your desired file naming convention. If you ever needed the fully qualified module name, you could use tyConModule, but that’s unnecessary here.
内容的提问来源于stack exchange,提问作者Thomas Mahler

