Как смоделировать mixins/несколько интерфейсов в Haskell?

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

Предположим, что ваша программа содержит несколько типов:

data Camera = Camera ...
data Light = SpotLight ... | DirectionalLight ...
data Object = Monster ... | Player ... | NPC ...

Теперь вы хотите реализовать некоторую базовую физику, поэтому вы хотите, чтобы все они имели положение и скорость, скажем, какого-то типа Vec3.

Один из способов сделать это - объявить класс Physical с функциями pos и vel и сделать все экземпляры своего типа. Но это означает, что вам нужно изменить все типы, чтобы содержать два Vec3 s, что раздражает, если у вас уже есть много хороших типов, и вы просто хотите приклеить немного функциональности сверху. Решение на основе объектива, предложенное Крисом Тейлором, имеет ту же проблему.

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

data Physical a = Physical a Vec3 Vec3

Тогда вам нужно только реализовать экземпляр pos, vel и Functor, и вы сможете сохранить все существующие объявления типов.

Однако... это не очень хорошо. Если теперь вы хотите иметь возможность рисовать свои объекты синим или зеленым или фиолетовым, вы можете сделать то же самое с цветами:

data Coloured a = Coloured a Colour

Но теперь, если у вас есть Coloured Physical Camera, вы должны fmap разное количество раз в зависимости от того, хотите ли вы посмотреть его цвет или его положение или его фокусное расстояние. И Coloured Physical Camera должен быть тем же самым, что и Physical Coloured Camera, но это не так. Так что это не изящное решение.

Есть ли хороший способ смешивания в разных наборах функций для типов в Haskell? Простое решение, которое работает в простом старом Haskell без языковых расширений или большого количества шаблонов, было бы идеальным, но я также открыт для изучения любой из библиотек, связанных с объективом, если это действительно лучший способ подойти к проблеме.

(Этот старый вопрос о повторном использовании кода в стиле mixins кажется связанным, но я боюсь, что не полностью понимаю вопрос или принятое решение.)

Ответ 1

Возможно, мы могли бы взять реплику из недооцененного пакета mtl и объединить два ранее предложенных подхода: объявить два типа конструкторов (и сделать их функторами) и объявить соответствующие типы/экземпляры.

Но вот трюк: мы создадим функторы, используя Data.Functor.Compose от transformers, а затем определим дополнительные "сквозные" чтобы сделать методы из внутренних слоев, доступных во внешнем слое. Точно так же, как mtl делает для монадных трансформаторов!

Во-первых, некоторые предварительные:

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE FlexibleInstances #-}

import Data.Functor.Compose

data Camera = Camera
data Light = SpotLight | DirectionalLight 
data Object = Monster | Player | NPC

data Vec3 = Vec3C -- dummy type 
data Colour = ColourC -- dummy type

Определения data:

data Physical a = Physical a Vec3 Vec3 deriving Functor
data Coloured a = Coloured a Colour deriving Functor

Соответствующие классы:

class Functor g => FunctorPhysical g where
    vecs :: g a -> (Vec3,Vec3)  

class Functor g => FunctorColoured g where
    colour :: g a -> Colour

Базовые экземпляры:

instance FunctorPhysical Physical where
    vecs (Physical _ v1 v2) = (v1,v2) 

instance FunctorColoured Coloured where
    colour (Coloured _ c) = c

И теперь трюк mtl. Прозрачные экземпляры!

instance Functor f => FunctorPhysical (Compose Physical f) where
    vecs (Compose f) = vecs f

instance Functor f => FunctorColoured (Compose Coloured f) where
    colour (Compose f) = colour f

instance FunctorPhysical f => FunctorPhysical (Compose Coloured f) where
    vecs (Compose (Coloured a _)) = vecs a

instance FunctorColoured f => FunctorColoured (Compose Physical f) where
    colour (Compose (Physical a _ _)) = colour a

Примерное значение:

exampleLight :: Compose Physical Coloured Light
exampleLight = Compose (Physical (Coloured SpotLight ColourC) Vec3C Vec3C) 

Вы должны иметь возможность использовать как vecs, так и colour с указанным выше значением.

РЕДАКТИРОВАТЬ: В приведенном выше решении есть проблема, что доступ к исходному обернутому значению является громоздким. Вот альтернативная версия, использующая comonads, которая позволяет использовать extract, чтобы вернуть завернутое значение.

import Control.Comonad
import Control.Comonad.Trans.Class
import Control.Comonad.Trans.Env
import Data.Functor.Identity

data PhysicalT w a = PhysicalT { unPhy :: EnvT (Vec3,Vec3) w a } 

instance Functor w => Functor (PhysicalT w) where
  fmap g (PhysicalT wa) = PhysicalT (fmap g wa)

instance Comonad w => Comonad (PhysicalT w) where
  duplicate (PhysicalT wa) = PhysicalT (extend PhysicalT wa)
  extract (PhysicalT wa) = extract wa

instance ComonadTrans PhysicalT where
  lower = lower . unPhy

--
data ColouredT w a = ColouredT { unCol :: EnvT Colour w a } 

instance Functor w => Functor (ColouredT w) where
  fmap g (ColouredT wa) = ColouredT (fmap g wa)

instance Comonad w => Comonad (ColouredT w) where
  duplicate (ColouredT wa) = ColouredT (extend ColouredT wa)
  extract (ColouredT wa) = extract wa

instance ComonadTrans ColouredT where
  lower = lower . unCol

class Functor g => FunctorPhysical g where
    vecs :: g a -> (Vec3,Vec3)  

class Functor g => FunctorColoured g where
    colour :: g a -> Colour

instance Comonad c => FunctorPhysical (PhysicalT c) where
    vecs = ask . unPhy

instance Comonad c => FunctorColoured (ColouredT c) where
    colour = ask . unCol

-- passthrough instances    
instance (Comonad c, FunctorPhysical c) => FunctorPhysical (ColouredT c) where
    vecs = vecs . lower

instance (Comonad c, FunctorColoured c) => FunctorColoured (PhysicalT c) where
    colour = colour . lower

-- example value
exampleLight :: PhysicalT (ColouredT Identity) Light
exampleLight = PhysicalT . EnvT (Vec3C,Vec3C) $ 
               ColouredT . EnvT ColourC       $ Identity SpotLight

К сожалению, для этого требуется еще больше шаблонов. Лично я бы просто использовал вложенные трансформаторы EnvT за счет менее равномерного доступа.

Ответ 2

Знаете ли вы, что у Tuple с arity of 2 есть экземпляр Functor, который отображает второй элемент? Мы можем использовать его в наших интересах.

data PositionAndVelocity = PositionAndVelocity Vec3 Vec3
data Colour = ...

f1 :: (PositionAndVelocity, Camera) -> ...
f2 :: (Colour, Camera) -> ...

Ответ 3

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

pos :: (r <: {_pos :: Vec3}) => (r, a) -> Vec3
pos (r, a) = r._pos

и т.д. Тогда цветная физическая камера будет просто значением типа (r, Camera), где r <: {_pos :: Vec3, _vel :: Vec3, _colour :: Colour}.

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

Ответ 4

Хотя я все еще подозреваю, что мы должны думать обо всем, думаем обо всем по-другому, менее OO-вдохновили, вот еще одно возможное решение. Я буду придерживаться примера Монстров, хотя 2D-графическая программа действительно является лучшим примером.

{-# LANGUAGE TypeFamilies, MultiParamTypeClasses, DeriveFunctor, FlexibleContexts #-}

import Control.Monad.Identity

class (Functor f, Functor (PropT f p)) => AttachProp f p where
  type PropT f p :: * -> *
  attachProp :: p -> f o -> PropT f p o
  detachProp :: PropT f p o -> (p, f o)

fmapProp :: (AttachProp f p, AttachProp f p')
  => f o -- dummy parameter (unevaluated), because type-functions aren't injective
         -> (p -> p') -> PropT f p o -> PropT f p' o
fmapProp q f pt = let (p, fo) = detachProp pt
                  in attachProp (f p) $ fo `asTypeOf` q


data R3Phys = R3Phys { position, momentum :: Vec3 }
data Colour = Colour

data Physical a = Physical R3Phys a deriving (Functor)
data Coloured a = Coloured Colour a deriving (Functor)
data PhysColoured a = PhysColoured Colour R3Phys a deriving (Functor)

instance AttachProp Identity R3Phys where
  type PropT Identity R3Phys = Physical
  attachProp rp = Physical rp . runIdentity
  detachProp (Physical rp o) = (rp, Identity o)
instance AttachProp Identity Colour where
  type PropT Identity Colour = Coloured
  attachProp c = Coloured c . runIdentity
  detachProp (Coloured c o) = (c, Identity o)
instance AttachProp Coloured R3Phys where
  type PropT Coloured R3Phys = PhysColoured
  attachProp rp (Coloured c o) = PhysColoured c rp o
  detachProp (PhysColoured c rp o) = (rp, Coloured c o)
instance AttachProp Physical Colour where
  type PropT Physical Colour = PhysColoured
  attachProp c (Physical rp o) = PhysColoured c rp o
  detachProp (PhysColoured c rp o) = (c, Physical rp o)

Обратите внимание, что PropT (PropT Identity R3Phys) Colour a и PropT (PropT Identity Colour) R3Phys a являются одним и тем же типом, а именно PhysColoured a. Конечно, нам нужно снова O (n²) экземпляров для n mixins. Легко можно было сделать с помощью шаблона Haskell, хотя, очевидно, вы должны подумать дважды, если хотите.

Ответ 5

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

Physical действительно совершенно естественен, как вы его предлагаете: a Monster, Camera и т.д. не имеет отдельной позиции, скорее позиция - это то, что вы получаете, комбинируя такой объект с некоторым пространством жить.

Но Coloured отличается, поскольку цвет является свойством самой вещи и, вероятно, будет иметь совершенно иное значение для монстра по сравнению с камерой, поэтому в отличие от Physical класс типов действительно выглядит разумным. Если вообще - возможно, было бы лучше просто использовать мономорфные функции для работы с различными видами цвета вручную.

Конечно, у вас может возникнуть соблазн подумать об этом так: сами вещи не окрашены, но они носят кожу с цветом. Я не думаю, что это должен быть единственный способ иметь цвет, но... достаточно справедливым, мы можем, очевидно, предоставить такой "скин", поэтому неокрашенные объекты тоже становятся красочными:

data ClSkin a = ClSkind { clSkinColour :: Colour
                        , clSkinned :: a         }
instance Coloured (Clsskin a) where
  colour = clSkinColour

Теперь вы говорите, что не важно, используете ли вы Physical (ClSkin a) или ClSkin (Physical a). Я говорю, что это имеет значение. Опять же, Physical - это комбинация между объектом и всем пространством, в котором он живет. Конечно, вы не хотите, чтобы это все пространство было окрашено! Так что действительно, Physical (ClSkin a) является единственным значимым вариантом. Или, альтернативно, вы можете сказать, что цвет - это то, что имеет смысл только для объектов в физическом пространстве. Ну, тогда вы просто сделаете цвет дополнительным полем этих данных!

data Physical a = Physical a Vec3 Vec3 (Maybe Colour)