Функциональные зависимости против семейства типов

Я разрабатываю среду для проведения экспериментов с искусственной жизнью и пытаюсь использовать семейства типов вместо функциональных зависимостей. Семейства типов являются предпочтительным подходом среди 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 может быть красным флагом.) (Этот код также компилирует OK.)

{-# 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

2 ответа

Решение

Я думаю, что вы слишком усложняете вещи. Не так сложно поддерживать любого актера во вселенной, это менее сложно.

Просто напишите свой 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,

Семейства типов против классов с несколькими параметрами с функциональными зависимостями

Чтобы ответить на вопрос в заголовке, функциональные зависимости, как правило, довольно противоречивы, так что заставить их работать - скорее головная боль. Семейства типов намного проще в использовании и настолько интуитивно понятны для функционального программиста, что я бы порекомендовал вам всегда сначала использовать их (если вам абсолютно не нужен дополнительный параметр типа для создания экземпляров какого-либо другого класса, над которым у вас нет контроля)).

В вашем случае, я не уверен, что вам это тоже нужно, поэтому я считаю, что вы сталкиваетесь с проблемами.

Что говорят ваши классы и примеры вселенной?

Оба определения класса связывают пользователя с использованием каждого юниверса по одному разу, тогда как им может потребоваться повторное использование юниверса с другим типом агента.

Давайте посмотрим, что происходит в ваших экземплярах вселенной:

  • Типы семейств: Вы создаете пример, написав множество шаблонов, просто повторно используя стандартный набор функций оптом. Это говорит о том, что вам не нужно было знать конкретный тип 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 ()

Здесь вы говорите, что вселенная должна принимать любой тип Агентов, а не один конкретный тип Агентов. Я переименовал Агент в Агенты, чтобы предложить пользователям использовать его для представления всех своих типов агентов в объединенном типе, как вы указали.

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

Здесь вы говорите, что тип Агентов должен иметь возможность взаимодействовать с любой вселенной.

Природа вселенной

Тот факт, что вы чувствуете, что вы можете написать декларации по умолчанию, как

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

или объявите экземпляр, который не использует никакой информации об ошибке:

instance Universe SimpleUniverse a where
    withAgent = ...

предлагает вам написать withAgent без ссылки на типы u или же a,

Предложение 2: Можете ли вы вообще отказаться от класса Universe в пользу любого типа TheUniverse, чтобы вы определили

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

который я не уверен, подойдет вам или...

Предложение 3: Откажитесь от ограничения класса Вселенной и сделайте withAgent работать с любым типом.

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

Трудно сказать, что лучше, не зная, какие другие функции вам нужны, но, надеюсь, одна из них может помочь. Я только предлагаю 2 и 3, потому что вы, кажется, говорите, что определение по умолчанию всегда работает. Возможно, в действительности некоторые функции должны быть в классе Вселенной, потому что они используют структуру вселенной, но не внутренние детали Агентов. Может быть, другие принадлежат к агентам, потому что, хотя они используют вселенную, они просто используют функции класса, а не внутренние детали. В любом случае имеем:

Общее предложение:

Тщательно продумайте, какой уровень детализации об агентах или вселенной необходим для функции. Если это и то и другое, возможно, вы могли бы выполнить рефакторинг на две отдельные вспомогательные функции, поэтому никакой функции не нужно знать внутреннюю работу как Вселенной, так и Агентов. Таким образом, вам не нужен класс типов, который имеет оба типа. Там не будет необходимости для TypeFamilies или FunDeps.

Другие вопросы по тегам