3

我正在开发一个用于运行人工生命实验的框架,并且我正在尝试使用类型族而不是函数依赖项。类型族似乎是 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函数内部,它可能会在宇宙中查询一个智能体列表,以便找到一个可能的交配伙伴。它将调用该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
4

2 回答 2

7

类型族与具有功能依赖性的多参数类型类

要回答标题中的问题,函数依赖往往是相当违反直觉的,所以让它们工作更让人头疼。类型族更容易使用,对函数式程序员来说也更直观,我建议你总是先尝试使用它们(除非你绝对需要额外的类型参数来创建你无法控制的其他类的实例)。

就您而言,我不确定您是否也需要,我相信这就是您遇到问题的原因。

你的 Universe 类和实例是怎么说的?

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

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

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

这让我怀疑你不需要将宇宙和代理如此紧密地联系起来。建议1:是否可以有两个独立但链接的类:

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

在这里,您是说 Universe 必须接受任何代理类型,而不是一种特定的代理类型。正如您所指出的,我已将 Agent 重命名为 Agents 以向用户建议他们使用它来表示联合类型中的所有代理类型。

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

在这里,您是说 Agents 类型必须能够与任何类型的 Universe 交互。

宇宙的本质

您觉得可以编写默认声明的事实,例如

defaultWithAgent :: (MyAgent u -> StateT u IO (MyAgent u)) -> String -> StateT u IO ()

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

instance Universe SimpleUniverse a where
    withAgent = ...

建议您可以withAgent在不参考类型ua.

建议 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 的结构,而不是 Agents 的内部细节。也许其他人属于代理,因为尽管他们使用 Universe,但他们只使用类函数,而不是内部细节。无论如何,我们有:

总体建议:

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

于 2012-10-18T18:15:26.547 回答
5

我认为你把事情复杂化了。支持宇宙中的每一种演员并不复杂,它不那么复杂

像这样写你的Universe类:

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

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

于 2012-10-18T16:58:28.137 回答