Эффективный, оптимальный, типизированный diff между данными произвольного типа
Глубокие теоретико-математические основания и лаконичный синтаксис 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
Разумеется, что патчи мы можем сериализовавыть и хранить в файле или распространять по Сети.
Подготовка⌗
Для работы нам понадобятся два пакета:
- Пакет
instant-generics
для генерации репрезентации (представления атомарными единицами) для произвольного типа. Рабочая для GHC8 версия доступна только на GitHub. - Пакет
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
.
Вот, собственно, и всё. Таким образом можно патчить сколь угодно ветвистые структуры данных, а также пользоваться тем фактом, что эндоморфизмы образуют моноид относительно операции их композиции.