Схемы рекурсии с использованием `Fix` для типа данных, который уже является функтором?

Я все еще работаю над своим текстовым редактором Rasa.

На данный момент я создаю систему для отслеживания окон / разделений (аналогично разделам в vim). Мне показалось естественным представить эту структуру в виде дерева:

data Dir = Hor
         | Vert
         deriving (Show)

data Window a =
  Split Dir SplitInfo (Window a) (Window a)
    | Single ViewInfo a
    deriving (Show, Functor, Traversable, Foldable)

Это отлично работает, я сохраняю свои View в дереве, а затем могу перемещаться по ним / fmap, чтобы изменить их, это также хорошо согласуется с пакетом линз!

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

Мне удалось понять это достаточно хорошо, чтобы построить версию Fixpoint:

data WindowF a r =
  Split Dir SplitInfo r r
    | Single ViewInfo a
    deriving (Show, Functor)

type Window a = Fix (WindowF a)

Однако теперь экземпляр Functor используется r;

Я пробовал несколько вариантов

deriving instance Functor Window

Но это задыхается, потому что окно - это синоним типа.

И:

newtype Window a = Window (Fix (WindowF a)) deriving Functor

И это тоже не удается;

• Couldn't match kind ‘* -> *’ with ‘*’
    arising from the first field of ‘Window’ (type ‘Fix (WindowF a)’)
• When deriving the instance for (Functor Window)
  1. Можно ли еще определить fmap / traverse для a? Или мне нужно выполнить эти операции с помощью примитивов схем рекурсии? Могу ли я реализовать Бифунктор? Как будет выглядеть реализация экземпляра?

Остальные типы: здесь, проект не компилируется, потому что у меня нет подходящего экземпляра Functor для Window ...

Спасибо!!


person Chris Penner    schedule 07.01.2017    source источник
comment
Да, это два вопроса в одном. Пожалуйста, не делай этого.   -  person dfeuer    schedule 07.01.2017
comment
Не уверен, о чем я думал, ха-ха, раз уж ты ответил на первый, я вставлю второй в новый. Спасибо, кстати!   -  person Chris Penner    schedule 08.01.2017


Ответы (2)


После долгой борьбы я пришел к выводу, что лучше всего определить два типа данных; стандартный тип данных, который имеет нужные вам свойства (в данном случае Bifunctor) и тип данных Recursive Functor, для которого вы можете определить экземпляры Base, Recursive и Corecursive.

Вот как это выглядит:

{-# language DeriveFunctor, DeriveTraversable, TypeFamilies  #-}

import Data.Typeable
import Data.Bifunctor
import Data.Functor.Foldable

data BiTree b l =
  Branch b (BiTree b l) (BiTree b l)
    | Leaf l
    deriving (Show, Typeable, Functor, Traversable, Foldable)

instance Bifunctor BiTree where
  bimap _ g (Leaf x) = Leaf (g x)
  bimap f g (Branch b l r) = Branch (f b) (bimap f g l) (bimap f g r)

data BiTreeF b l r =
  BranchF b r r
    | LeafF l
    deriving (Show, Functor, Typeable)

type instance Base (BiTree a b) = BiTreeF a b
instance Recursive (BiTree a b) where
  project (Leaf x) = LeafF x
  project (Branch s l r) = BranchF s l r

instance Corecursive (BiTree a b) where
  embed (BranchF sp x xs) = Branch sp x xs
  embed (LeafF x) = Leaf x

Теперь вы можете использовать свой базовый тип (BiTree) во всем коде, как обычно; и когда вы решите использовать схему рекурсии, вам просто нужно помнить, что при распаковке вы используете F-версии конструкторов:

anyActiveWindows :: Window -> Bool
anyActiveWindows = cata alg
  where alg (LeafF vw) = vw^.active
        alg (BranchF _ l r) = l || r

Обратите внимание, что если вы в конечном итоге перестроите набор окон, вы все равно будете использовать версии NON-F в правой части =.

Для своего сценария я определил следующее, и он отлично работает; У меня есть Functor и Bifunctor для Window, как я и хотел, даже не используя newtype:

type Window = BiTree Split View

data SplitRule =
  Percentage Double
  | FromStart Int
  | FromEnd Int
  deriving (Show)

data Dir = Hor
        | Vert
        deriving (Show)

data Split = Split
  { _dir :: Dir
  , _splitRule :: SplitRule
  } deriving (Show)

makeLenses ''Split

data View = View
  { _active :: Bool
  , _bufIndex :: Int
  } deriving (Show)

makeLenses ''View
person Chris Penner    schedule 10.01.2017

Да, вы хотите использовать версию Fix от Data.Bifunctor.Fix:

newtype Fix p a = In { out :: p (Fix p a) a }

instance Bifunctor p => Functor (Fix p) where
  fmap f (In x) = In (bimap (fmap f) f x)

Вам нужно будет изменить свой WindowF тип, чтобы он соответствовал:

data WindowF r a =
  Split Dir SplitInfo r r
    | Single ViewInfo a
    deriving (Show, Functor)

instance Bifunctor WindowF where
  bimap f _g (Split dir si x y) = Split dir si (f x) (f y)
  bimap _f g (Single vi a) = Single vi (g a)

newtype Window a = Window (Fix WindowF a) deriving Functor

С этим можно использовать recursion-schemes вместе со вспомогательным типом:

import Data.Functor.Foldable hiding (Fix (..))
import Data.Profunctor.Unsafe
import Data.Coerce

newtype Flip p a b = Flip {unFlip :: p b a}

instance Bifunctor p => Bifunctor (Flip p) where
  bimap f g (Flip x) = Flip (bimap g f x)

instance Bifunctor p => Functor (Flip p a) where
  fmap = coerce (first :: (x -> y) -> p x a -> p y a)
    :: forall x y . (x -> y) -> Flip p a x -> Flip p a y

type instance Base (Fix p a) = Flip p a
instance Bifunctor p => Recursive (Fix p a) where
  project = Flip #. out
  cata f = f . Flip . first (cata f) . out

К сожалению, определение Recursive для версии с оболочкой newtype немного сложнее:

newtype Window a = Window {getWindow :: Fix WindowF a} deriving (Functor)
type instance Base (Window a) = Flip WindowF a

instance Recursive (Window a) where
  project = coerce #. project .# getWindow
  cata = (. getWindow) #. cata
person dfeuer    schedule 07.01.2017
comment
Нужен ли нам ньютайп? Что это дает в этом случае? Это необходимо для экземпляра Functor? У меня возникли проблемы с использованием этого сейчас, например, следующий (тривиальный) пример работает с обычным Fix, но не с новым Bifunctor Fix, взгляните на allTree здесь: gist.github.com/ChrisPenner/aa6083478d2d1100f62a974860aae529 Извините за все проблемы, я довольно новичок в этой проблеме. две разные версии этого не делают проще, ха-ха. - person Chris Penner; 08.01.2017
comment
@ChrisPenner, это поможет? - person dfeuer; 08.01.2017
comment
Это значительно сложнее, чем я думал; Я думаю, что могу вернуться к моему простому решению Functor, если у вас нет более простых идей. Я хотел бы изучить схемы рекурсии, но если что-то настолько простое, как наличие типа данных Functor, является настолько сложным, то я не знаю, справляюсь ли я с этой задачей. Я действительно ценю объяснения! По какой-то причине стек отказывается устанавливать библиотеку profunctors, поэтому у меня даже возникают проблемы с ее опробованием. Несмотря на это, даже если бы я мог это понять, думаю, другим программистам было бы сложно понять результат :( - person Chris Penner; 09.01.2017
comment
@ChrisPenner, вы, безусловно, можете использовать подход recursion-schemes и написать экземпляр Functor вручную. Я подозреваю, что самый красивый способ может заключаться в написании бифункционального клона recursion-schemes, но это определенно кажется излишним. - person dfeuer; 09.01.2017
comment
Хммм, интересно; Я только что заново открыл для себя раздел Plated библиотеки объективов; у него есть para, и вы можете использовать transformOf как cata. Возможно, он немного отличается, но имеет аналогичную мощность без необходимости искажать вашу базовую структуру данных, что приятно. Пока это сработало для моих нужд. - person Chris Penner; 09.01.2017
comment
Возникли несколько проблем с этим покрытием, потому что функции, подобные transform, имеют подпись (a -> a) -> a -> a, которая не позволяет изменять тип как часть операций сворачивания, таких как cata ... вздох. Спасибо за попытку! - person Chris Penner; 10.01.2017
comment
В конце концов снова пошел в другом направлении; смотри мой ответ; Мне это проще понять, чем Data.Bifunctor.Fix; дайте мне знать, что вы об этом думаете. - person Chris Penner; 10.01.2017