Глубокие теоретико-математические основания и лаконичный синтаксис Haskell делают этот язык одним из наиболее совершенных средств осмысления и составления компьютерных программ за всю историю кибернетики.

В данной короткой заметке мы воспользуемся пакетами для Обобщённого Программирования (Generic Programming) и покажем, как можно получать разницу (diff) между данными некоторого типа, а также накладывать эту разницу (patch) для получения эндоморфизма типа.

Задача

Положим, мы имеем следующий тип автомобилей:

data Color = White | Black

data Car = Car
  { carMaker :: String
  , carColor :: Color
  , carPrice :: Int
  }

Наша задача состоит в получении разницы (диффа, дельты) между двумя автомобилями, белым “Volvo” и чёрным “BMW”:

> let c1 = Car "Volvo" White 10000
c1 :: Car
> let c2 = Car "BMW" Black 20000
c2 :: Car
>
> let d = diff c1 c2
d :: Generics.Instant.GDiff.EditScript

Данным “патчем” мы теперь можем манипулировать как захотим, получая от компилятора полную гарантию корректности на уровне системы типов. В частности, мы можем наложить патч на автомобиль “Volvo” (на c1), чтобы получить искомую “BMW” (машину c2):

> print c1
Car {carMaker = "Volvo", carColor = White, carPrice = 10000}
> patch d c1
Just (Car {carMaker = "BMW", carColor = Black, carPrice = 20000})
> patch d c1 == Just c2
True

Разумеется, что патчи мы можем сериализовавыть и хранить в файле или распространять по Сети.

Подготовка

Для работы нам понадобятся два пакета:

  1. Пакет instant-generics для генерации репрезентации (представления атомарными единицами) для произвольного типа. Рабочая для GHC8 версия доступна только на GitHub.
  2. Пакет gdiff-ig для собственно функционала diff и patch. На самом деле в оригинальном пакете gdiff тоже есть свои средства для создания репрезентации типа, однако они не так удобны, как полуавтоматическая генерация, которую я предлагаю ниже. Рабочая версия доступна на моём GitHub.

Таким образом, после добавления зависимостей в .cabal файл, в package.yaml проекта можно внести что-то вроде:

extra-deps:
- archive: https://github.com/serras/instant-generics/archive/70c0adc23518b07f57a22f66f236c5b0dee34cbe.zip
- archive: https://github.com/nbrk/gdiff-ig/archive/3af627dcabf41ca30485a1cb9fc33d72a4c8196c.zip

Генерация экземпляров тайпклассов

Преамбула

В первую очередь, включим нужные расширения языка и опишем собственно наш тип (Car), патчами к которому мы планируем манипулировать:

{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE TemplateHaskell       #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE TypeFamilies          #-}

import           Generics.Instant
import           Generics.Instant.GDiff
import           Generics.Instant.TH

data Color = White | Black

data Car = Car
  { carMaker :: String
  , carColor :: Color
  , carPrice :: Int
  }

Генерация репрезентации с instant-generics

Следующим шагом, мы воспользуемся функцией deriveAll из новых версий пакета instant-generics для автоматического порождения репрезентации (т.е. реализаций тайпклассов Constructor и Representable) нашего сложного типа Car:

deriveAll ''Color
deriveAll ''Car

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

Полуавтоматическое порождение GDiff для нашего типа

Последним шагом, мы используем код из пакета gdiff-ig для порождения (деривации) экземпляров тайпклассов SEq, Build, Children и, наконец, GDiff для нашего сложного типа Car. Как и прежде, мы должны проделать деривации для всех составляющих пользовательских типов (типа Color в нашем примере). Делаем мы это следующим образом:

instance SEq      Color where shallowEq  = shallowEqDef
instance Build    Color where build      = buildDef
instance Children Color where children   = childrenDef
instance GDiff Color

instance SEq      Car where shallowEq  = shallowEqDef
instance Build    Car where build      = buildDef
instance Children Car where children   = childrenDef
instance GDiff Car

Всё. Теперь мы можем спокойно диффать автомобили Car и цвета Color друг с другом! В самом простом случае для этого используются функции diff и patch.

Использование diff

Дельта между данными одинакового типа (экземплара GDiff) получаются функцией:

diff :: GDiff a => a -> a -> EditScript

Она возвращает тип EditScript, который-то и содержит патч к нашим данным (на самом деле, к репрезентации).

Использование patch

Полученную дельту, т.е. данные типа EditScript, мы накладываем с помощью функции:

patch :: GDiff a => EditScript -> a -> Maybe a

При неуданом патчинге она возращает Nothing.

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