Аппликативный комонадический (анти)контекст: 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