Как смоделировать 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)