Я разрабатываю структуру для проведения экспериментов с искусственной жизнью и пытаюсь использовать семейства типов вместо функциональных зависимостей. Семейства типов, похоже, являются предпочтительным подходом среди 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"
Редактировать: пытаясь представить простой пример, я пропустил часть мотивации для своего дизайна.
Роль № 1, которую играет класс Universe, — это сериализация и десериализация агентов, поэтому я думаю, что он должен быть связан с классом Agent. Он также имеет функции readAgent
и writeAgent
. Однако я хотел гарантировать, что пользователь не сможет случайно забыть написать агент после его изменения, поэтому вместо экспорта этих функций я предоставляю функцию withAgent
, которая позаботится обо всем. Функция withAgent
принимает два параметра: функцию, которая выполняется на агенте, и имя (уникальный идентификатор) агента, на котором выполняется программа. Он читает файл, содержащий этот агент, запускает программу и записывает обновленный агент обратно в файл. (Вместо этого я мог бы просто экспортировать функции readAgent и writeAgent.)
Существует также класс Daemon
, который отвечает за предоставление каждому агенту справедливой доли ЦП. Таким образом, внутри основного цикла демона он запрашивает у вселенной текущий список агентов. Затем для каждого агента вызывается функция 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
withAgent
) будет для определенного типа агента, верно? т.е. Я бы назвалwithAgent eatBugFood "bug5"
гдеeatBugFood :: Bug -> StateT SimpleUniverse IO Bug
где указан хотя бы фактический тип агента (например,u
все еще может быть свободным/неуказанным)? Затем вы все еще можете вызвать правильную функциюreadAgent
для этого конкретного агента внутриwithAgent
, что может показаться волшебством, но оно будет работать. 19.10.2012Agent
. Например, вDaemon
вы можете просто хранитьliveALittle
функции каждого загруженного агента вместо фактическихAgent
, что делает ненужной количественную оценку существования. Однако вам все равно понадобится отличный контейнер для хранения всех загруженныхAgent
; вероятно, с участием классаTypeable
. 19.10.2012