我正在开发一个用于运行人工生命实验的框架,并且我正在尝试使用类型族而不是函数依赖项。类型族似乎是 Haskellers 中的首选方法,但我遇到了一种情况,函数依赖似乎更合适。我错过了一个技巧吗?这是使用类型族的设计。(此代码编译正常。)
{-# LANGUAGE TypeFamilies, FlexibleContexts #-}
import Control.Monad.State (StateT)
class Agent a where
agentId :: a -> String
liveALittle :: Universe u => a -> StateT u IO a
-- plus other functions
class Universe u where
type MyAgent u :: *
withAgent :: (MyAgent u -> StateT u IO (MyAgent u)) ->
String -> StateT u IO ()
-- plus other functions
data SimpleUniverse = SimpleUniverse
{
mainDir :: FilePath
-- plus other fields
}
defaultWithAgent :: (MyAgent u -> StateT u IO (MyAgent u)) -> String ->
StateT u IO ()
defaultWithAgent = undefined -- stub
-- plus default implementations for other functions
--
-- In order to use my framework, the user will need to create a typeclass
-- that implements the Agent class...
--
data Bug = Bug String deriving (Show, Eq)
instance Agent Bug where
agentId (Bug s) = s
liveALittle bug = return bug -- stub
--
-- .. and they'll also need to make SimpleUniverse an instance of Universe
-- for their agent type.
--
instance Universe SimpleUniverse where
type MyAgent SimpleUniverse = Bug
withAgent = defaultWithAgent -- boilerplate
-- plus similar boilerplate for other functions
有没有办法避免强迫我的用户编写最后两行样板文件?与下面使用fundeps 的版本相比,这似乎让我的用户更简单。(UndecideableInstances 的使用可能是一个危险信号。)(此代码也可以编译。)
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances,
UndecidableInstances #-}
import Control.Monad.State (StateT)
class Agent a where
agentId :: a -> String
liveALittle :: Universe u a => a -> StateT u IO a
-- plus other functions
class Universe u a | u -> a where
withAgent :: Agent a => (a -> StateT u IO a) -> String -> StateT u IO ()
-- plus other functions
data SimpleUniverse = SimpleUniverse
{
mainDir :: FilePath
-- plus other fields
}
instance Universe SimpleUniverse a where
withAgent = undefined -- stub
-- plus implementations for other functions
--
-- In order to use my framework, the user will need to create a typeclass
-- that implements the Agent class...
--
data Bug = Bug String deriving (Show, Eq)
instance Agent Bug where
agentId (Bug s) = s
liveALittle bug = return bug -- stub
--
-- And now my users only have to write stuff like...
--
u :: SimpleUniverse
u = SimpleUniverse "mydir"
编辑:在试图展示一个简单的例子时,我省略了我设计的部分动机。
Universe 类扮演的#1 角色是序列化和反序列化代理,所以我认为它必须与 Agent 类相关联。它也有readAgent
和writeAgent
功能。但是,我想确保用户在修改后不会意外忘记编写代理,因此我没有导出这些函数,而是提供了一个withAgent
处理所有事情的函数。该withAgent
函数有两个参数:一个在代理上运行的函数,以及在其上运行程序的代理的名称(唯一 ID)。它读取包含该代理的文件,运行程序,然后将更新的代理写回到文件中。(我可以改为导出 readAgent 和 writeAgent 函数。)
还有一个Daemon
类负责为每个代理分配公平的 CPU 份额。因此,在守护进程的主循环中,它会在 Universe 中查询当前的代理列表。然后,对于每个代理,它调用withAgent
函数来运行该liveAlittle
代理的程序。守护进程不关心代理是什么类型。
该函数还有另一个用户withAgent
:代理本身。在智能体的liveALittle
函数内部,它可能会在宇宙中查询一个智能体列表,以便找到一个可能的交配伙伴。它将调用该withAgent
函数来运行某种交配函数。显然,一个代理只能与同一物种(类型类)的另一个代理交配。
编辑:这是我想我会使用的解决方案。不是类型族或函数依赖项,但现在我必须做一些事情,以便编译器知道liveALittle
要调用哪个。我这样做的方法是让用户提供正确liveALittle
的参数。
{-# LANGUAGE DeriveGeneric #-}
import Control.Monad.State (StateT)
import Data.Serialize (Serialize)
import GHC.Generics (Generic)
class Agent a where
agentId :: a -> String
liveALittle :: Universe u => a -> StateT u IO a
-- plus other functions
class Universe u where
-- Given the name of an agent, read it from a file, and let it run.
withAgent :: (Agent a, Serialize a) =>
(a -> StateT u IO a) -> String -> StateT u IO ()
-- plus other functions
-- This method will be called by a daemon
daemonTask :: (Universe u, Agent a, Serialize a) =>
(a -> StateT u IO a) -> StateT u IO ()
daemonTask letAgentLiveALittle = do
-- do some stuff
withAgent letAgentLiveALittle "a"
-- do some other stuff
data SimpleUniverse = SimpleUniverse
{
mainDir :: FilePath
-- plus other fields
}
instance Universe SimpleUniverse where
withAgent = undefined -- stub
-- plus implementations for other functions
--
-- And now my users only have to write stuff like...
--
data Bug = Bug String deriving (Show, Eq, Generic)
instance Serialize Bug
instance Agent Bug where
agentId (Bug s) = s
liveALittle bug = return bug -- stub