我目前正在尝试使用类型类进行实验,作为练习,能够在各种上下文中记录日志的能力 (即在IO上下文中打印到控制台)。我开始通过实现我的 Logger 作为由各种用于记录日志的函数组成的类型类来实现这一点,这样我就可以为IO单子定义一个实例,但也为其他单子的上下文中添加附加实现留出空间。
最终结果是:
-- |Class / wrapper for convenient use within another monad.
class Logger m where
-- |Logs an error message /(prefixed with the '__[ERROR]__' tag)/
logError :: String -> m ()
-- |Logs a warning message /(prefixed with the '__[WARNING]__' tag)/
logWarning :: String -> m ()
-- |Logs a success message /(prefixed with the '__[SUCCESS]__' tag)/
logSuccess :: String -> m ()
-- |Logs an informative message /(prefixed with the '__[INFO]__' tag)/
logInfo :: String -> m ()
-- |Logs a regular message /(i.e with no prefix)/
logMsg :: String -> m ()
-- |Instance of logger in the IO monad
instance Logger IO where
logError = printError
logWarning = printWarning
logSuccess = printSuccess
logInfo = printInfo
logMsg = printMsg
-- |Instance of logger for a state
instance (MonadIO m) => Logger (StateT s m) where
logError = liftIO . printError
logWarning = liftIO . printWarning
logSuccess = liftIO . printSuccess
logInfo = liftIO . printInfo
logMsg = liftIO . printMsg
这在当时似乎是个好主意(由于来自面向对象编程背景,我会倾向于将所有东西都变成“类”,即使不应该这样做)。
我意识到我同样可以直接定义带有类型限制的日志函数并处理完毕,例如:
logError :: (MonadIO m) => String -> m ()
logError = liftIO . printError
对于其他函数也是这样,我将拥有可以在任何基于IO的monad中调用的东西...
显然,两种解决方案都有其优点和权衡。
我的 Logger 类型类的用例是否可以被认为是 "滥用",或者我通过这种方式实现它的想法是正确的吗?(我理解类型类允许使用特定多态性,这正是我想到的)
我读过一个限制,我仍在努力完全理解它,即对于任何给定类型,只能有一个类型类实例,因此在我的情况下,我已经定义了 StateT 的实例,它位于 IO monad 中,这意味着我失去了覆盖具有相同签名的后续状态的能力。我知道这个警告,但我很难想象这会成为一个具体的问题。
另一方面,简单的基于函数的方法同样优雅易用,尽管它阻止了在不定义要在不同上下文中使用的全新函数的情况下重写行为。 当函数也能轻松完成工作时,类型类应该只在最后一步使用/编写吗? 我希望能对这两种方法获得一些洞见和反馈。
提前感谢。
StateT
实例应该有一个不同的限制条件:instance Logger m => Logger (StateT s m) where logError = lift . logError
,等等。 - dfeuer