功能依赖与类型族

时间:2012-10-18 13:39:01

标签: haskell functional-dependencies type-families

我正在开发一个用于运行人工生命实验的框架,我正在尝试使用类型系列而不是函数依赖项。类型族似乎是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类。它还具有readAgentwriteAgent功能。但是,我想确保用户在修改代理后不会意外忘记编写代理,因此我提供withAgent函数来处理所有内容,而不是导出这些函数。 withAgent函数有两个参数:在代理上运行的函数,以及运行程序的代理的名称(唯一ID)。它读取包含该代理的文件,运行该程序,并将更新的代理写回文件。 (我可以只导出readAgent和writeAgent函数。)

还有一个Daemon类负责为每个代理提供其公平的CPU份额。因此,在守护程序的主循环中,它会查询Universe以获取当前的代理列表。然后,对于每个代理,它调用withAgent函数来运行该代理的liveAlittle程序。守护进程不关心代理的类型。

withAgent函数还有一个其他用户:代理本身。在代理的liveALittle函数内部,它可能会查询Universe以查找代理列表,以便成为可能的交配伙伴。它将调用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

2 个答案:

答案 0 :(得分:7)

类型系列与具有功能依赖关系的多参数类型类

要回答标题中的问题,功能依赖性往往是反直觉的,因此让它们工作更令人头痛。 类型系列更容易使用,而且更直观 一个功能性程序员,我建议你总是先尝试使用它们(除非你绝对需要 额外的类型参数,用于制作您无法控制的其他类的实例)。

在你的情况下,我不确定你是否也需要,我相信,为什么你会遇到问题。

您的Universe类和实例说了什么?

两个类定义都将用户绑定到使用每个Universe一次,而他们可能希望重用具有不同代理类型的Universe。

让我们看看你的Universe实例中发生了什么:

  • 类型系列:您通过编写大量样板文件来重新使用a来创建实例 标准功能批发。这表明您不需要知道特定类型 MyAgent处理它。在任何函数上似乎都不是代理上下文。嗯。
  • 功能依赖:您使用instance Universe SimpleUniverse a where...并且神奇地使用 您的Agent Bug实例为您提供了一个有效的Universe。这是因为您的实例声明 使用类型a所以在等式的匹配末尾,没有使用关于a的任何事实。

这使我怀疑你不需要如此强烈地链接宇宙和代理。 建议1:是否可以有两个独立但相互关联的类:

class Universe u where
   withAgents :: Agents a => (a -> StateT u IO a) -> String -> StateT u IO ()

此处您说宇宙必须接受任何代理类型,而不是一个特定的代理类型。 我已将代理重命名为代理,向用户建议他们使用代理所有代理类型 在你指出的联合类型中。

class Agents a where
   agentId :: a -> String
   liveALittle :: Universe u => a -> StateT u IO a

在这里,您要说Agent类型必须能够与任何类型的Universe进行交互。

宇宙的本质

您觉得可以编写

等默认声明
defaultWithAgent :: (MyAgent u -> StateT u IO (MyAgent u)) -> String -> StateT u IO ()

或声明一个不使用任何有关Bug的信息的实例:

instance Universe SimpleUniverse a where
    withAgent = ...

建议您可以在不参考withAgentu类型的情况下撰写a

建议2:你可以完全放弃Universe类,而选择TheUniverse类型 所以你定义

withAgent :: (Agents a => a -> StateT TheUniverse IO a) -> String -> StateT TheUniverse IO ()

我不相信会适合你,或者......

建议3:完全抛弃Universe类限制,并使withAgent任何类型一起使用。

withAgent :: (Agents a => a -> StateT u IO a) -> String -> StateT u IO ()

如果不知道你需要什么其他功能,很难说什么是最好的,但希望其中一个可能会有所帮助。 我只提出建议2和3,因为您似乎在说默认定义始终有效。 也许实际上有些函数需要在Universe类中,因为它们使用Universe的结构而不是代理的内部细节。也许其他人属于Agent,因为虽然他们使用Universe,但他们只使用类函数,而不是内部细节。无论如何我们都有:

总体建议:

仔细考虑功能需要的Agent或Universe的详细程度。如果是两者,也许您可​​以重构为两个单独的辅助函数,因此没有函数需要知道Universe和代理的内部工作方式。这样您就不需要具有这两种类型的类型类。不需要TypeFamilies或FunDeps。

答案 1 :(得分:5)

我认为你过分复杂了。支持宇宙中的每一种演员都不是更多复杂,它更少复杂。

只需写下你的Universe课程:

class Universe u where
  withAgent :: Agent a => (a -> StateT u IO a) -> String -> StateT u IO ()

请注意,您不必使用函数依赖项或多参数类型类,因为a不必在类头中进入范围;它由Agent a => ...纳入范围。这也是你在功能相关版本中所做的事情,因为即使你使用u a | u -> aa实际上并没有在类体中使用;相反,Agent a => ...会影响外部a