Как избежать уродливого кода, решающего эту проблему в Haskell (расширения LANGUAGE)? - PullRequest
5 голосов
/ 12 сентября 2011

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

Я упростил то, что пытаюсь написать, в следующем скелете:

module Structure0 where
type Message = String
class Creature a where
    processInput :: a -> Message -> Message
class World a where
    processAction :: a -> b -> Message -> a
    getCreatures  :: a -> [b]

---- USAGE EXAMPLE ----
data Parrot = Parrot Int deriving Show
instance Creature Parrot where
    processInput p s = s
data ParrotWorld = ParrotWorld [Parrot]
instance World ParrotWorld where
    processAction w p s = w
    getCreatures (ParrotWorld ps) = ps

В этом коде я хотел бы, чтобы параметр b в определении класса World мог принимать все значения данных, принадлежащие классу Creature, что-то вроде:

processAction :: (Creature b) => a -> b -> Message -> a

Конечно, эти примеры не являются 'Фактический код haskell, давайте рассмотрим два решения, которые я нашел: первое, включающее ExistentialQuantification:

{-# LANGUAGE ExistentialQuantification #-}
module Structure1 where
type Message = String
class Creature_ a where
    processInput :: a -> Message -> Message
data Creature = forall c. Creature_ c => Creature c
instance Creature_ Creature where
    processInput (Creature c) = processInput c
class World a where
    processAction :: a -> Creature -> Message -> a
    getCreatures  :: a -> [Creature]

---- USAGE EXAMPLE ----
data Parrot = Parrot Int deriving Show
instance Creature_ Parrot where
    processInput u s = s
data ParrotWorld = ParrotWorld [Creature]
instance World ParrotWorld where
    processAction w p s = w
    getCreatures (ParrotWorld ps) = ps

и второе, предложенное добрым парнем из #haskell, с использованием TypeFamilies:

{-# LANGUAGE TypeFamilies, FlexibleContexts #-}
module Structure2 where
type Message = String
class Creature a where
    processInput :: a -> Message -> Message
class (Creature (WorldCreature a)) => World a where
    type WorldCreature a :: *
    processAction :: a -> WorldCreature a -> Message -> a
    getCreatures  :: a -> [WorldCreature a]
---- USAGE EXAMPLE ----
data Parrot = Parrot Int deriving Show
instance Creature Parrot where
    processInput p s = s
data ParrotWorld = ParrotWorld [Parrot]
instance World ParrotWorld where
    type WorldCreature ParrotWorld = Parrot
    processAction w p s = w
    getCreatures (ParrotWorld ps) = ps

Основная цель этого упражнения - написание красивого, элегантного кода.Итак, вопросы:

1) Должен ли я выражать Существо как Класс вместо Данных?(Я делаю это, потому что Creature - это просто вещь, которая реализует функцию processInput, и многие фактические реализации Creature сильно различаются; особенно во время прототипирования, я бы не хотел постоянно менять способ соответствия шаблону Creature.

2) Первое решение, которое я предоставляю, немного уродливо из-за того, что поддерживается версия Creature и Creature_.Преимущество, однако, в том, что я могу написать смешанный список типа [Существо];Проблема в том, что я не могу сопоставить шаблон с объектом, например:

\(Creature (Parrot x)) -> x

не удастся из-за системы типов.Могу ли я сделать это хорошо?

3) Второе решение имеет проблему расширяемости: скажем, я хотел бы построить Мир с двумя типами существ, скажем, Parrot1 и Parrot2: как я мог бы написать код в этомcase?

4) Я структурирую код с неправильной точки зрения?Могу ли я получить элегантное решение, просто используя простой haskell?

Спасибо всем:)

Carlo

1 Ответ

2 голосов
/ 12 сентября 2011

1 класс против данных

Существо должно быть классом - оно описывает интерфейс. Данные следует использовать, когда вы думаете о фактическом сообщении значений или когда вам нужно ввести новый тип, оборачивая существующий объект новым поведением. Например, монаде Identity необходимо обернуть ее значения в новый тип, иначе вы увидите instance Monad a для всех a, что может привести к конфликту с созданием чего-либо еще Monad экземпляра. Но, возможно, вам придется обернуть его.

2 списка

Есть способ сделать это с Data.Dynamic, но каждый раз, когда я думал о том, чтобы сделать это таким образом, я мог придумывать способ сделать это с обычными классами типов вместо этого. Тем не менее, я не так много написал на Haskell, и многие библиотеки, безусловно, полагаются на Data.Dynamic. Если вы действительно хотите распаковать тип, вам, вероятно, нужно его использовать.

3 экстенсиональность

Как и раньше, если вы можете оставить функциональность, зависящую от типа, в классах, это лучше всего. Было бы очень полезно, если бы вы могли опубликовать пример, показывающий, почему вы не можете добавить другую функцию в Creature. Я предполагаю, что вы хотите сосчитать numParrots в приведенном ниже примере, и вам действительно нужно их распаковать.

4 общих комментария

Всегда есть много решений проблемы. Исходя из вашего описания, я думаю, что «разные миры должны повлечь за собой разные типы сообщений», а не то, что мир должен быть привязан к определенному типу существ (например, ParrotWorld).

другое решение

вот мое решение, используя Data.Typeable. Как уже упоминалось выше, я впервые использую его, так что, возможно, есть более чистый способ.

{-# LANGUAGE DeriveDataTypeable,
             ImpredicativeTypes,
             NoMonomorphismRestriction,
             RankNTypes,
             ScopedTypeVariables #-}

module Test where

import Data.Typeable

type Message = String

class Typeable α => Creature α where
    processInput :: α -> Message -> Message

-- box a creature
type BoxedC = (Message -> Message, Typeable β => Maybe β)
boxC :: Creature α => α -> BoxedC
boxC x = (processInput x, cast x)

class World α where
    -- from your description, I'd not have Creature as part of this.
    processAction :: α -> Message -> α
    getCreatures :: α -> [BoxedC]

data Parrot = Parrot { parrotMessage :: String } deriving Typeable
data Lizard = Lizard { lizardMessage :: String } deriving Typeable

instance Creature Parrot where processInput p _ = (parrotMessage p)
instance Creature Lizard where processInput l _ = (lizardMessage l)

-- NOTE: Keep it simple and use a single World instance
-- (i.e. no typeclass) unless you need it.
data BaseWorld = BaseWorld { creatureList :: [BoxedC] }
instance World BaseWorld where
    processAction w _ = w
    getCreatures = creatureList

w = BaseWorld [boxC $ Parrot "parrot1", boxC $ Lizard "Lizard1"]

numParrots :: [BoxedC] -> Int
numParrots lst = foldl (+) 0 (map (go . snd) lst) where
    go :: (forall β. Typeable β => Maybe β) -> Int
    go (Just x :: Maybe Parrot) = 1
    go _ = 0

test = numParrots (getCreatures w)

Идея похожа на вашу: мы помещаем существ в коробку, прежде чем поместить их в список. Элементы в штучной упаковке имеют достаточно данных, чтобы вы могли распаковать тип, если вам нужно. И последнее, что нужно упомянуть, хотя, возможно, это не то, что вам нужно, это то, что замыкания являются мощными. Вам не нужно вести список существ, если вы можете выразить их результаты в виде композиции функций. Например, в псевдокоде у вас может быть функция

bind_creature :: Creature -> World -> World

, который добавляет существо в мир, и у World есть тип, который возвращает его следующую итерацию,

data World = World { nextWorld :: World }

, который вы установили себе для базы, а именно w = World w. Для простоты предположим, что у каждого существа есть функция

transformWorld :: Creature -> World -> World

тогда вы можете реализовать bind_creature, как,

bind_creature c w = World { nextWorld = transformWorld c (nextWorld w) }

надеюсь, это поможет.

...