首先,您确定 Slots 真的想在特定线程中执行吗?在 Haskell 中编写线程安全的代码很容易,而且 GHC 中的线程非常轻量级,因此将所有事件处理程序执行绑定到特定的 Haskell 线程并不会获得太多收益。
另外,mkSlot 的回调不需要给定 Slot 本身:您可以使用 recursive do-notation 在其回调中绑定 slot,而无需担心与 mkSlot 打结。
无论如何,您不需要像那些解决方案那样复杂的东西。我希望当您谈论存在类型时,您正在考虑通过 TChan(您提到在 cmets 中使用)发送类似 (a -> IO (), a) 的内容并在另一端应用它,但您希望 TChan 到接受任何 a 的这种类型的值,而不仅仅是一个特定的 a。这里的关键见解是,如果你有 (a -> IO (), a) 并且不知道 a 是什么,你唯一能做的就是将函数应用于值,给你一个 IO () - 所以我们可以直接通过频道发送!
这是一个例子:
import Data.Unique
import Control.Applicative
import Control.Monad
import Control.Concurrent
import Control.Concurrent.STM
newtype SlotGroup = SlotGroup (IO () -> IO ())
data Signal a = Signal Unique (TVar [Slot a])
data Slot a = Slot Unique SlotGroup (a -> IO ())
-- When executed, this produces a function taking an IO action and returning
-- an IO action that writes that action to the internal TChan. The advantage
-- of this approach is that it's impossible for clients of newSlotGroup to
-- misuse the internals by reading the TChan or similar, and the interface is
-- kept abstract.
newSlotGroup :: IO SlotGroup
newSlotGroup = do
chan <- newTChanIO
_ <- forkIO . forever . join . atomically . readTChan $ chan
return $ SlotGroup (atomically . writeTChan chan)
mkSignal :: IO (Signal a)
mkSignal = Signal <$> newUnique <*> newTVarIO []
mkSlot :: SlotGroup -> (a -> IO ()) -> IO (Slot a)
mkSlot group f = Slot <$> newUnique <*> pure group <*> pure f
connect :: Signal a -> Slot a -> IO ()
connect (Signal _ v) slot = atomically $ do
slots <- readTVar v
writeTVar v (slot:slots)
emit :: Signal a -> a -> IO ()
emit (Signal _ v) a = atomically (readTVar v) >>= mapM_ (`execute` a)
execute :: Slot a -> a -> IO ()
execute (Slot _ (SlotGroup send) f) a = send (f a)
这使用TChan 将操作发送到每个插槽所绑定的工作线程。
请注意,我对 Qt 不是很熟悉,所以我可能错过了模型的一些微妙之处。您也可以通过以下方式断开插槽:
disconnect :: Signal a -> Slot a -> IO ()
disconnect (Signal _ v) (Slot u _ _) = atomically $ do
slots <- readTVar v
writeTVar v $ filter keep slots
where keep (Slot u' _) = u' /= u
如果这可能是一个瓶颈,你可能想要Map Unique (Slot a) 而不是[Slot a]。
因此,这里的解决方案是 (a) 认识到您有一些基本上基于可变状态的东西,并使用可变变量来构造它; (b) 意识到函数和 IO 动作就像其他所有东西一样是一流的,所以你不必做任何特殊的事情来在运行时构造它们:)
顺便说一句,我建议保留Signal 和Slot 的实现抽象,不要从定义它们的模块中导出它们的构造函数;毕竟,有很多方法可以在不更改 API 的情况下解决这种方法。