Аппликативный комонадический (анти)контекст: ComonadApply
Типы⌗
Предположим, что мы программируем некую компьютерную симуляцию и решили выделить следующие типы в нашей программе:
- Тип, моделирующий внутреннее состояние сущности (допустим, юнита в компьютерной игрей):
data Unit = Unit
{ _unitName :: String
, _unitPosition :: L.V2 Double
}
- Тип, моделирующий более абстрактную, внешнюю (например, административную) информацию, присущую вообще всем объектам симуляции:
data Object s = forall d . Object
{ _objectId :: Int
, _objectState :: s
, _objectIntelDB :: [d]
-- , ...
}
Предпосылки⌗
Уже на этапе описания типов мы можем предположить, какие виды существующих функций лягут в низкоуровневую основу нашей программы. Например:
- функции над
String
иInt
- линейная алгебра из пакета linear (векторы
L.V2
и т.д.) - функции над внутренним состоянием
s
объекта - и т.д.
Очевидно, что особого внимания заслуживает конкретный тип s
и его проекция из
“обёрточного” типа Object s
. Должно быть, у нас будет много комбинаторов над
внутренним состоянием юнита (типом s
), поскольку именно оно будет содержать наименование,
позицию, цвет, очки опыта и т.п., присущее юниту. Более того, эндоморфизм s ->s
описывает эволюцию, т.е. динамику всего юнита.
Как минимум, нам удобно следующее:
- иметь лифтинг унарных операторов над
s
вObject s
(функций(a -> b) -> Object a -> Object b
) - выделять
s
изObject s
известным способом в любой момент - иметь лифтинг и вычисление N-арных операторов над
s
в последовательностиObject s
(например, функций(a -> b -> c) -> Object a -> Object b -> c
)
Очевидно, что с первым пунктом справится подходящая реализация функтора,
т.е. функция fmap
.
instance Functor Object where
fmap f (Object oi ost idb) = Object oi (f ost) idb
Аппликативные функторы, затем, позволят нам производить лифтинг и вычисления N-арных операторов:
instance Applicative Object where
pure x = Object (-1) x []
(<*>) (Object _oi ostf _odb) (Object oi ost odb) =
Object oi (ostf ost) odb
Однако у нас не будет средств выделять полученный результат c
из Object c
!
Сигнатура аппликативного лифтинга бинарной операции выглядит несколько иначе:
(a -> b -> c) -> Object a -> Object b -> Object c
Против желаемой нами:
(a -> b -> c) -> Object a -> Object b -> c
Комонады⌗
Именно данную возможность “вытягивания из контекста” и предоставляют комонады (монады с перевёрнутыми стрелками):
class Functor w => Comonad w where
extract :: w a -> a
duplicate :: w a -> w (w a)
-- можно задать вместо `duplicate`:
extend :: (w a -> b) -> w a -> w b
Реализуем этот интерфейс для нашего типа, где в качестве extract
просто зададим проекцию
“важного” типа s
:
instance Comonad Object where
extract = _objectState
duplicate (Object oi ost idb) =
Object oi (Object oi ost idb) idb
Однако одна лишь комонада не даст нам возможности выполнять лифтинг
операторов. Для этого потребуется реализация дополнительного тайпкласса
ComonadApply
:
class Comonad w => ComonadApply w where
(<@>) :: w (a -> b) -> w a -> w b
К счастью, комбинатор (<@>)
полностью синонимичен (<*>)
из аппликативного
функтора, а значит для создания экземпляра ComonadApply (Object a)
нам
достаточно всего лишь одной строчки:
instance ComonadApply Object where
Результат⌗
Как результат мы имеем комонадические комбинаторы, которые позволяют нам легко приспособить существующий конкретный код для использования на более высоком уровне абстракции.
Имея представления о состоянии юнита, его позиции и расчёте расстояний между
юнитами, мы можем легко адаптировать код для некой абстрактной объемлющей структуры Object
,
которая и не должна ничего знать о манипуляциях c позициями и расстояниями.
-- для "истинного" юнита
distanceUnit :: Unit -> Unit -> Double
distanceUnit u1 u2 =
L.distance (_unitPosition u1) (_unitPosition u2)
-- для объектов-юнитов:
distance :: Object Unit -> Object Unit -> Double
distance o1 o2 =
extract $ distanceUnit <$> o1 <@> o2