Типы

Предположим, что мы программируем некую компьютерную симуляцию и решили выделить следующие типы в нашей программе:

  1. Тип, моделирующий внутреннее состояние сущности (допустим, юнита в компьютерной игрей):
data Unit = Unit
  { _unitName     :: String
  , _unitPosition :: L.V2 Double
  }
  1. Тип, моделирующий более абстрактную, внешнюю (например, административную) информацию, присущую вообще всем объектам симуляции:
data Object s = forall d . Object
  { _objectId      :: Int
  , _objectState   :: s
  , _objectIntelDB :: [d]
  -- , ...
  }

Предпосылки

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

  • функции над String и Int
  • линейная алгебра из пакета linear (векторы L.V2 и т.д.)
  • функции над внутренним состоянием s объекта
  • и т.д.

Очевидно, что особого внимания заслуживает конкретный тип s и его проекция из “обёрточного” типа Object s. Должно быть, у нас будет много комбинаторов над внутренним состоянием юнита (типом s), поскольку именно оно будет содержать наименование, позицию, цвет, очки опыта и т.п., присущее юниту. Более того, эндоморфизм s ->s описывает эволюцию, т.е. динамику всего юнита.

Как минимум, нам удобно следующее:

  1. иметь лифтинг унарных операторов над s в Object s (функций (a -> b) -> Object a -> Object b)
  2. выделять s из Object s известным способом в любой момент
  3. иметь лифтинг и вычисление 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