Какой алгебраический шаблон соответствует этому типу дерева?

У меня есть головоломка для вас,

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

Я разрабатываю систему компоновки для своего текстового редактора Rasa; Он использует разделение очень похоже на Vim. Я решил описать сплиты с помощью дерева; вы можете представить его как двоичное дерево с вертикальным или горизонтальным разделением с «представлениями» в конечных узлах. Эта фотография может помочь.

Вот моя исходная структура данных:

data Direction = Hor | Vert
data Tree a = 
  Branch Direction (Tree a) (Tree a)
  | Leaf a
  deriving (Functor)

Некоторые из операций, которые мне нужны:

  • split :: (View -> Tree View) -> Tree View -> Tree View Который разбивает узлы (или нет) на два узла по горизонтали или вертикали (при сохранении их положения в дереве)
  • close :: (View -> Bool) -> Tree View -> Tree View, который "закрывает" все представления, соответствующие предикату, удаляя их из дерева и соответствующим образом реорганизовывая смежные представления.
  • fmap; Я бы хотел, чтобы дерево было функтором, чтобы я мог изменять представления.

Некоторые приятные функции: - focusRight :: Tree View -> Tree View, Делает вид активным тогда и только тогда, когда ближайший горизонтально соединенный вид слева БЫЛ активен.

Я ищу абстракцию или набор абстракций, которые обеспечили бы эту функциональность чистым способом. Вот мой мыслительный процесс:

Сначала я подумал, что у меня моноид. Идентификацией было пустое дерево, и mappend просто прикрепляло бы другую ветвь к дереву, но это не работает, так как у меня есть две операции: добавление по вертикали и добавление по горизонтали, а операции не ассоциативный, когда они смешаны вместе.

Затем я подумал: «Некоторые из моих операций зависят от их контекста», поэтому, вероятно, у меня есть Comonad. Версия дерева, которая у меня есть, не работает как со-монада, потому что у меня нет значения extract для ветки, поэтому я реструктурировал свое дерево следующим образом:

data Tree a = Node Direction [Tree a] a [Tree a]
    deriving (Functor)

но это все еще не обрабатывало случай «разделения» узла на основе того, что было внутри него, это соответствовало сигнатуре (View -> Tree View) -> Tree View -> Tree View, которая объединяется с bind из Монады, так что, может быть, у меня была монада? Я могу реализовать монаду для исходного определения дерева, но не могу понять это для моей версии дерева Comonad.

Есть ли способ получить лучшее из обоих миров здесь? Я выкапываю неправильное дерево с помощью Comonad/Monad? В основном я ищу элегантный способ моделирования этих функций в моей структуре данных. Спасибо!

Если вы хотите увидеть полный код, функции здесь, а текущее дерево — здесь.


person Chris Penner    schedule 19.03.2017    source источник
comment
У @pigworker есть целая беседа и хороший кусок бумага все об использовании монад (индексированных наборов) для разделения 2D-пространства   -  person Benjamin Hodgson♦    schedule 19.03.2017
comment
@benjamin-hodgson О, здорово, спасибо! TBH на первый взгляд кажется, что бумага немного не в моей лиге; Я только что коснулся поверхности семейств шрифтов; так что программирование на уровне типов, описанное в этой статье, будет для меня чем-то недостижимым. Тем не менее, я обязательно посмотрю видео и попробую прочитать газету! Простые решения также приветствуются :)   -  person Chris Penner    schedule 19.03.2017
comment
Касательное предложение: рассмотрите возможность использования LambdaCase в вашем коде recursion-schemes. YMMV, но я считаю, что это делает определения (ко) алгебр намного проще для глаз.   -  person duplode    schedule 20.03.2017
comment
@ChrisPenner Понятно! Резюме в горшке: ваш первый Tree (чей Monad прививает деревья вместе) идет по правильному пути, но это не работает, потому что удерживать размеры составляющих ящиков в линию сложно. (Если вы замените a на b с помощью fmap, вам лучше убедиться, что b не занимают больше 2D-пространства, чем a!) Идея Линдли и Макбрайда состоит в том, чтобы соединить блоки вместе на уровне типов и предоставить интерфейс индексированной монады, который запрещает пользователям искривлять пространство-время. FWIW Я думаю, что статья о хасохизме является отличным введением в причудливые типы в целом, а не только в эту проблему.   -  person Benjamin Hodgson♦    schedule 20.03.2017


Ответы (2)


Я бросил попытки втиснуть это в комментарий. Конор Макбрайд имеет целый разговор и, с Сэмом Линдли, большой кусок бумага, все об использовании монад для разделения 2D-пространства. Поскольку вы попросили элегантное решение, я чувствую себя обязанным дать вам краткое изложение их работы, хотя я бы не обязательно советовал встраивать это в вашу кодовую базу - я подозреваю, что, вероятно, проще просто работать с библиотекой, такой как boxes и настройте логику обрезки и изменения размера с помощью ручной обработки ошибок.


Ваш первый Tree — это шаг в правильном направлении. Мы можем написать экземпляр Monad для прививки деревьев вместе:

instance Monad Tree where
    return = Leaf
    Leaf x >>= f = f x
    Branch d l r >>= f = Branch d (l >>= f) (r >>= f)

Tree's join берет дерево с деревьями на его листьях и позволяет пройти весь путь до самого дна, не останавливаясь на полпути вниз. Может быть полезно думать о Tree как о бесплатная монада, как показал @danidiaz в ответе. Или Кметт может сказать, что у вас очень простой синтаксис, позволяющий подставлять термины, Var которых называется Leaf.

В любом случае, суть в том, что вы можете использовать >>= для выращивания деревьев, постепенно обрезая их листья. Здесь у меня есть одномерный пользовательский интерфейс (давайте пока забудем о Direction) с одним окном, содержащим String, и, многократно разрезая его пополам, я получаю восемь окон меньшего размера.

halve :: [a] -> Tree [a]
halve xs = let (l, r) = splitAt (length xs `div` 2) xs
         in Node (Leaf l) (Leaf r)

ghci> let myT = Leaf "completeshambles"
-- |completeshambles|
ghci> myT >>= halve
Node (Leaf "complete") (Leaf "shambles")
-- |complete|shambles|
ghci> myT >>= halve >>= halve
Node (Node (Leaf "comp") (Leaf "lete")) (Node (Leaf "sham") (Leaf "bles"))
-- |comp|lete|sham|bles|
ghci> myT >>= halve >>= halve >>= halve
Node (Node (Node (Leaf "co") (Leaf "mp")) (Node (Leaf "le") (Leaf "te"))) (Node (Node (Leaf "sh") (Leaf "am")) (Node (Leaf "bl") (Leaf "es")))
-- |co|mp|le|te|sh|am|bl|es|

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

Проблема в том, что Tree не понимает того факта, что физическое пространство является ограниченным и ценным ресурсом. fmap позволяет заменить a на b, но полученная структура не поместится на экране, если b будут занимать больше места, чем a!

ghci> fmap ("in" ++) myT
Leaf "incompleteshambles"

Это становится более серьезным в двух измерениях, потому что коробки могут толкать друг друга и рваться. Если размер среднего окна случайно изменяется, я получаю либо коробку деформированной формы, либо дыру в середине (в зависимости от того, где она находилась в дереве).

+-+-+-+         +-+-+-+            +-+-+  +-+
| | | |         | | | |            | | |  | |
+-+-+-+         +-+-+-++-+   or,   +-+-+--+-+
| | | |  ---->  | |    | | perhaps | |    | |
+-+-+-+         +-+-+-++-+         +-+-+--+-+
| | | |         | | | |            | | |  | |
+-+-+-+         +-+-+-+            +-+-+  +-+

Расширение окна — вполне разумная вещь, но в реальном мире пространство, в которое оно расширяется, должно откуда-то браться. Вы не можете увеличить одно окно, не сжав другое, и наоборот. Это не та операция, которую можно выполнить с помощью >>=, выполняющего локальные замены в отдельных листовых узлах; вам нужно посмотреть на братьев и сестер окна, чтобы узнать, кто занимает пространство, прилегающее к нему.


Таким образом, вам не должно быть разрешено использовать >>= для изменения размера контента. Идея Линдли и Макбрайда состоит в том, чтобы научить специалистов по проверке шрифтов соединять коробки вместе. Используя натуральные числа на уровне типов и сложение,

data Nat = Z | S Nat
type family n :+ m where
    Z :+ m = m
    S n :+ m = S (n :+ m)

они работают с контентом, проиндексированным по его ширине и высоте. (В статье используются двумерные матрицы, представленные в виде векторов векторов, но для эффективности вы можете использовать массив с фантомным типом, измеряющим его размер.)

a, Box a :: (Nat, Nat) -> *
-- so Box :: ((Nat, Nat) -> *) -> (Nat, Nat) -> *

Для размещения двух блоков рядом с помощью Hor требуется, чтобы они имели одинаковую высоту, а для размещения их друг над другом с помощью Ver требуется, чтобы они имели одинаковую ширину.

data Box a wh where
    Content :: a '(w, h) -> Box a '(w, h)
    Hor :: Box a '(w1, h) -> Box a '(w2, h) -> Box a '(w1 :+ w2, h)
    Ver :: Box a '(w, h1) -> Box a '(w, h2) -> Box a '(w, h1 :+ h2)

Теперь мы готовы построить монаду для склеивания этих деревьев. Семантика return не изменилась — он сам помещает 2D-объект в Box.

return :: a wh -> Box a wh
return = Content

Теперь давайте подумаем о >>=. Как правило, коробка состоит из нескольких частей Content разного размера, составленных таким образом, чтобы получилась большая коробка. Ниже у меня есть три фрагмента контента размером 2x1, 2x2 и 1x3, составляющие блок 3x3. Это поле будет выглядеть примерно так: Hor (Ver (Content 2x1) (Content 2x2)) Content 1x3.

 2x1
+--+-+
|  | |
+--+ |1x3
|  | |
|  | |
+--+-+
 2x2

Хотя вы, абонент >>=, знаете внешние размеры своей коробки, вы не знаете размеров отдельных частей содержимого, из которых она состоит. Как вы можете ожидать сохранения размера содержимого, когда вы вырезаете его с помощью >>=? Вам придется написать функцию, которая сохраняет размер без априорного знания о том, каков был размер.

Итак, >>= берет Box известного размера wh, разбирает его на части, чтобы найти содержимое, обрабатывает его с помощью функции, которая сохраняет (неизвестный) размер содержимого, которое вы ему даете*, и собирает его обратно, чтобы создать новую коробку с тот же размер wh. Обратите внимание на тип ранга 2, отражающий тот факт, что вызывающая сторона >>= не имеет контроля над размерами содержимого, с которым будет вызываться продолжение.

(>>=) :: Box a wh -> (forall wh2. a wh2 -> Box b wh2) -> Box b wh
Content x >>= f = f x
Hor l r >>= f = Hor (l >>= f) (r >>= f)
Ver t b >>= f = Ver (t >>= f) (b >>= f)

Если вы используете синоним типа ~> для функций, сохраняющих индекс, и переворачиваете аргументы, вы получаете что-то похожее на =<< для обычных Monad, но со стрелкой другого типа. Композиция Клейсли тоже выглядит весьма симпатично.

type a ~> b = forall x. a x -> b x

return :: a ~> Box a
(=<<) :: (a ~> Box b) -> (Box a ~> Box b)
(>=>) :: (a ~> Box b) -> (b ~> Box c) -> (a ~> Box c)

Так что это монады над индексированными наборами. (Подробнее читайте в Kleisli Arrows of Outrageous Fortune< /a>.) В бумаге они создают еще куча инфраструктуры для поддержки обрезки и перестановки блоков, что, вероятно, будет полезно для создания пользовательского интерфейса. Для повышения эффективности вы также можете решить отслеживать текущее окно с помощью застежка-молния, что является забавным упражнением. Между прочим, я думаю, что Hasochism — отличное введение в причудливые типы в целом, а не только как решение этой конкретной проблемы.

*Если предположить, что индекс a действительно является точным показателем его физического размера

person Benjamin Hodgson♦    schedule 20.03.2017
comment
Спасибо! Я прочитаю это как можно скорее, просто хотел, чтобы вы знали, что пространство для отслеживания, выделенное для каждого ящика, на самом деле не очень важно в моем случае; структура каких ящиков добавлена ​​к какому в каком направлении самая важная часть :) - person Chris Penner; 20.03.2017
comment
@ChrisPenner Что ж, в этом случае исходного экземпляра Tree и его экземпляра Monad должно быть достаточно (и, надеюсь, первая половина моего ответа является достаточно хорошим объяснением того, как это работает), но как вы предлагаете избежать случайного изменения размера содержимого? - person Benjamin Hodgson♦; 20.03.2017
comment
Это для текстового редактора, блоки - это области просмотра в буферах, я думаю, что окна просмотра должны изменять свой размер каждый раз, когда одно из них разделено или закрыто. В любом случае мы можем гарантировать, что изменим размер не более чем на 1 окно. Как мы можем использовать монаду для реализации close? - person Chris Penner; 20.03.2017
comment
Я очень ценю, что вы приложили все усилия, чтобы объяснить это подробно! Я рад погрузиться в это завтра :) Я надеялся углубиться в программирование типов, это должно помочь мне начать! - person Chris Penner; 20.03.2017
comment
@ChrisPenner IDK ваш точный вариант использования, но я думаю о случае, когда fmap случайно изменяет размер a в одном из ваших конечных узлов и сдвигает другие окна - см. Диаграмму примерно в середине поста. close должно быть легко - если вы закроете поле 1x3 в моем примере выше, два других поля должны расшириться, чтобы занять его место. Я ожидаю, что есть только один способ проверить тип: заменить родителя закрытого окна его родным братом после расширения ближайшего дочернего элемента (детей) на ту же величину. Сейчас 3 часа ночи, поэтому я буду рад ответить на любые дополнительные вопросы завтра :) - person Benjamin Hodgson♦; 20.03.2017
comment
Я буду использовать интуитивно понятный экземпляр Monad для разделения, но, вероятно, буду придерживаться моей реализации схем рекурсии для других операций, поскольку кажется, что для этой формы операции нет четкой абстракции. Этот материал по теории типов действительно классный (и я обязательно вернусь к нему), но он немного тяжеловат для моего варианта использования. Спасибо за вашу помощь :) Я оставлю его открытым еще на несколько дней на случай, если у других будут комментарии, но тогда я вернусь и приму! - person Chris Penner; 22.03.2017

Я бы представил ваш тип как монаду и использовал бы >>= для обработки split.

{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free

data Direction = Hor | Vert

data TreeF a = TreeF Direction a a deriving Functor

type Tree a = Free TreeF a

Что касается close, я бы, вероятно, использовал cata или para из recursion-schemes, поскольку close, по-видимому, работает снизу вверх и требует максимум знаний о родительских узлах и родственных узлах. Вы также можете обратиться к Control.Lens.Plated .

Кстати, у Free уже есть Recursive экземпляр. FreeF TreeF a будет соответствующей алгеброй. Но вы упомянули, что это не очень хорошо сработало.

Работа напрямую с конструкторами Free и FreeT может оказаться затруднительной. Возможно, здесь могли бы помочь некоторые синонимы шаблонов.

person danidiaz    schedule 19.03.2017