如何在GHC Haskell中实现Discord机器人库的类型安全插件?
类型安全的Discord机器人插件库问题
我最近在开发一个用于Discord机器人的Haskell库,核心目标是让用户能编写类型安全的插件。Discord发送的消息结构大致是这样的:
data EventType = FooEvent | BarEvent deriving (Eq, Show) data Payload = Payload { eventType :: EventType , payload :: Maybe Value }
这里的eventType参数唯一决定了payload里JSON对象的类型,部分EventType可能没有payload。
期望实现的效果
我希望做到两点:
- 用户可以在类型层面声明要处理的事件类型,类型系统会强制要求提供对应类型的处理函数;
- 所有不同类型的插件能被当作同质实体统一处理,收到消息时,每个插件尝试把
payload的JSON值转换为对应类型,转换成功就运行插件代码。
现有代码情况
示例Payload类型
我定义了对应事件的Payload类型,并实现了JSON序列化实例:
data FooPayload = FooPayload {foo :: String} deriving (Read, Eq, Show, Generic) instance ToJSON FooPayload instance FromJSON FooPayload data BarPayload = BarPayload {bar :: String} deriving (Read, Eq, Show, Generic) instance ToJSON BarPayload instance FromJSON BarPayload
带关联类型的类型类
我创建了带关联类型的Convertible类型类,用来处理事件类型和Payload类型的转换:
class FromJSON (PayloadType ev) => Convertible (ev :: EventType) where type PayloadType ev :: * convert :: Value -> Maybe (PayloadType ev) convert = parseMaybe parseJSON -- 下面这个run函数的注释版本无法通过类型检查 -- run :: Proxy (PayloadType ev) -> Plugin ev s -> Value -> IO () run :: Plugin ev s -> Value -> IO () run p v = undefined -- case convert v of -- Just v' -> runPlugin p v' -- 因为类型族的非内射性,这里通不过类型检查 -- Nothing -> return ()
对应的实例实现:
instance Convertible 'FooEvent where type PayloadType 'FooEvent = FooPayload instance Convertible 'BarEvent where type PayloadType 'BarEvent = BarPayload
插件相关类型定义
我还定义了插件的类型,以及用于统一处理的包装类型:
data Plugin (ev :: EventType) s = Convertible ev => Plugin { initializePlugin :: IO s , runPlugin :: PayloadType ev -> IO () } newtype RunnablePlugin = RunnablePlugin (Hide Plugin) data Hide f = forall (ev :: EventType) s. Convertible ev => Hide (f ev s)
示例插件
我写了几个测试插件,并且可以把它们放到统一的列表里:
fooPlugin :: Plugin 'FooEvent () fooPlugin = Plugin { initializePlugin = return () , runPlugin = \_ -> putStrLn "This is a foo plugin" } barPlugin :: Plugin 'BarEvent () barPlugin = Plugin { initializePlugin = return () , runPlugin = \_ -> putStrLn "This is a bar plugin" } plugins :: [RunnablePlugin] plugins = [ RunnablePlugin $ Hide fooPlugin , RunnablePlugin $ Hide barPlugin ]
理想的运行逻辑
我希望能实现这样的runPlugins函数:
runPlugins :: Value -> [RunnablePlugin] -> IO () runPlugins val plugs = do forM_ plugs $ \(RunnablePlugin (Hide p)) -> do run p val return ()
比如调用runPlugins (toJSON (FooPayload "foo")) plugins时,应该输出This is a foo plugin。
当前遇到的问题
目前除了Convertible类型类里注释掉的run函数实现外,其他代码都能通过类型检查。我猜测问题出在统一处理插件的时候丢失了类型信息,没办法恢复,但我对这类类型编程不太熟悉。
想请教大家:这个需求能不能用标准的GHC Haskell实现?还是必须依赖依赖类型?
内容的提问来源于stack exchange,提问作者identity
相关产品推荐
相关产品推荐

