Squeak.ru - шаблоны программирования

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

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

  • Можно ли использовать каждого актера в каждой вселенной, или, как говорит ваш код, каждая вселенная может использоваться только одним типом актера? 18.10.2012
  • Я не возражаю, если каждая вселенная может использоваться только одним экземпляром агента, потому что пользователь может использовать преимущества алгебраических типов данных для поддержки более чем одного вида агентов. У меня был подход с использованием экзистенциальных типов, который позволил бы использовать несколько экземпляров агента во вселенной. Однако этот подход подвергся критике (я думаю, справедливо) за то, что он не соответствует хаскелю. 18.10.2012

Ответы:


1

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

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

18.10.2012
  • Я добавил некоторые пояснения к моему вопросу выше. Функция withAgent считывает агент из файла. Он будет вызываться демоном, который не знает или не заботится о типе агента. Если файлы могут содержать разные типы агентов, юниверс не будет знать тип агента в файле, а значит, не будет знать, как его десериализовать. Вот почему я думаю, что класс Universe может поддерживать только один класс типов агентов. Э-э, если я не вернусь к обертыванию агентов в экзистенциальные типы. 19.10.2012
  • Но функция преобразования агента (первый аргумент withAgent) будет для определенного типа агента, верно? т.е. Я бы назвал withAgent eatBugFood "bug5" где eatBugFood :: Bug -> StateT SimpleUniverse IO Bug где указан хотя бы фактический тип агента (например, u все еще может быть свободным/неуказанным)? Затем вы все еще можете вызвать правильную функцию readAgent для этого конкретного агента внутри withAgent, что может показаться волшебством, но оно будет работать. 19.10.2012
  • Единственная ситуация, в которой вам по-прежнему необходимо использовать семейства типов или функциональные зависимости, — это если вы действительно хотите обеспечить, чтобы каждый юниверс содержал только один тип Agent. Например, в Daemon вы можете просто хранить liveALittle функции каждого загруженного агента вместо фактических Agent, что делает ненужной количественную оценку существования. Однако вам все равно понадобится отличный контейнер для хранения всех загруженных Agent; вероятно, с участием класса Typeable. 19.10.2012
  • В итоге я использовал что-то очень похожее на ваше последнее предложение (см. конец отредактированного вопроса выше). 19.10.2012

  • 2

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

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

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

    Что говорят ваши классы и экземпляры Universe?

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

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

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

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

    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. Полностью снимите ограничение класса Universe и заставьте withAgent работать с любым типом.

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

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

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

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

    18.10.2012
  • Вы дали мне хорошую пищу для размышлений. Добавил уточнение в конец своего вопроса, но рассмотрю другие варианты оформления с учетом ваших предложений. 19.10.2012
  • Новые материалы

    Угловая структура архитектуры
    Обратите внимание, что эта статья устарела, я решил создать новую с лучшей структурой и с учетом автономных компонентов: https://medium.com/@marekpanti/angular-standalone-architecture-b645edd0d54a..

    «Данные, которые большинство людей используют для обучения своих моделей искусственного интеллекта, поставляются со встроенным…
    Первоначально опубликовано HalkTalks: https://hacktown.com.br/blog/blog/os-dados-que-a-maioria-das-pessoas-usa-para-treinar-seus-modelos-de-inteligencia-artificial- ja-vem-com-um-vies-embutido/..

    Сильный ИИ против слабого ИИ: различия парадигм искусственного интеллекта
    В последние годы изучению и развитию искусственного интеллекта (ИИ) уделяется большое внимание и прогресс. Сильный ИИ и Слабый ИИ — две основные парадигмы в области искусственного интеллекта...

    Правильный способ добавить Firebase в ваш проект React с помощью React Hooks
    React + Firebase - это мощная комбинация для быстрого и безопасного создания приложений, от проверки концепции до массового производства. Раньше (знаете, несколько месяцев назад) добавление..

    Создайте API с помощью Python FastAPI
    Создание API с помощью Python становится очень простым при использовании пакета FastAPI. После установки и импорта вы можете создать приложение FastAPI и указать несколько конечных точек. Каждой..

    Веселье с прокси-сервером JavaScript
    Прокси-серверы JavaScript — это чистый сахар, если вы хотите создать некоторую общую логику в своих приложениях, чтобы облегчить себе жизнь. Вот один пример: Связь клиент-сервер Мы..

    Получить бесплатный хостинг для разработчиков | Разместите свой сайт за несколько шагов 🔥
    Статические веб-сайты — это веб-страницы с фиксированным содержанием и его постоянным содержанием. Но теперь статические сайты также обрабатывают динамические данные с помощью API и запросов...