Как сопоставить функции с RoseTree в Applicative (Haskell)?

Как применить аппликатив к RoseTree, т.е. вернуть дерево, состоящее из деревьев, созданных последовательным применением функций к начальным узлам. Вот код, который я написал:

{-# LANGUAGE DeriveFunctor, InstanceSigs #-}

data RoseTree a = Nil | Node a [RoseTree a] deriving(Functor,Show)

instance Applicative RoseTree where
    pure :: a -> RoseTree a
    pure x = Node x []

    (<*>) :: RoseTree (a -> b) -> RoseTree a -> RoseTree b
    (<*>) _ Nil = Nil
    (<*>) Nil _ = Nil
    (<*>) (Node f tree) (Node x subtrees) = Node (f x) (zipWith (<*>) tree subtrees)

Я не уверен, что не так с моим определением чистого и (‹*>). Вот ошибка, которую я получил:

Ошибка: failure in expression `(Node (+1) []) <*> (Node 7 [Node 1 [], Node 2 [], Node 3 [Node 4 []]])' expected: Node 8 [Node 2 [],Node 3 [],Node 4 [Node 5 []]] but got: Node 8 []

Тестовые случаи для справки:

-- >>> (Node (+1) [Node (*2) []]) <*> Nil
-- Nil
--
-- >>> Nil <*> (Node 7 [Node 1 [], Node 2 [], Node 3 [Node 4 []]])
-- Nil
--
-- >>> (Node (+1) []) <*> (Node 7 [Node 1 [], Node 2 [], Node 3 [Node 4 []]])
-- Node 8 [Node 2 [],Node 3 [],Node 4 [Node 5 []]]
--
-- >>> (Node (+1) [Node (*2) []]) <*> (Node 5 [Node 2 [], Node 8 [Node 1 []]])
-- Node 6 [Node 3 [],Node 9 [Node 2 []],Node 10 [Node 4 [],Node 16 [Node 2 []]]]

person llamaro25    schedule 16.09.2019    source источник


Ответы (2)


Типы могут иметь более одного действительного экземпляра Applicative (например, как списки имеют один непосредственно в [], а другой - в их newtype оболочке ZipList). Ваша функция <*> кажется допустимой для применимого экземпляра, но не для того, который вам нужен в соответствии с вашими тестовыми примерами (а также не для того, который использует это определение pure).

Проблема здесь:

(<*>) (Node f tree) (Node x subtrees) = Node (f x) (zipWith (<*>) tree subtrees)

С ним связаны три основные проблемы, учитывая то, что ожидают ваши тестовые примеры:

  1. Он никогда не применяет f ни к чему в subtrees.
  2. Он никогда ничего не применяет в tree к x.
  3. Он применяет каждый элемент tree только к одному элементу в subtrees.

Вместо этого должна работать эта строка:

(<*>) (Node f tree) n@(Node x subtrees) = Node (f x) (map (fmap f) subtrees ++ map (<*> n) tree)

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

person Joseph Sible-Reinstate Monica    schedule 16.09.2019
comment
У меня есть серьезные сомнения по поводу (<*>) _ Nil = Nil. Я не пробовал проверять законы, но это как-то не так. - person dfeuer; 16.09.2019
comment
@dfeuer Я не вижу ничего, что могло бы пойти не так. Единственный случай, который, как я предвижу, может потерпеть неудачу, - это закон композиции, когда u, v и w все Node с вещами в списках. - person Joseph Sible-Reinstate Monica; 16.09.2019
comment
Я имею в виду не столько в отношении законов (хотя я бы не был шокирован, если бы они как-то нарушались), сколько в отношении какой-то связной концепции. Подумайте, что произойдет, если вы просто удалите это уравнение. - person dfeuer; 16.09.2019
comment
@dfeuer Тогда pure id <*> Nil будет внизу, поскольку ни один регистр не соответствует, и тогда это определенно будет незаконным. - person Joseph Sible-Reinstate Monica; 16.09.2019
comment
Пункты 1,2,3 меня не совсем убеждают. Я имею в виду, что ZipList является аппликативным, но применяет каждую функцию к одному элементу и наоборот точечным образом, эффективно усекая списки до одинаковой длины. Может быть, мы можем реализовать (законный?) ZipRoseTree аппликативный пример, где деревья обрезаются до их наибольшей общей формы, а затем приложение точечно, как для почтовых списков. Если это также законно, возникает вопрос: какой экземпляр нужен OP? - person chi; 16.09.2019
comment
@chi Эти баллы были получены из тестовых случаев спрашивающего. Я почти уверен, что мы могли бы внедрить законное ZipRoseTree, но я не вижу возможности, чтобы такая штука могла пройти эти тесты. - person Joseph Sible-Reinstate Monica; 16.09.2019
comment
А, это хороший момент. Я пропустил тестовые примеры, которые исключают экземпляр zip-дерева. - person chi; 16.09.2019
comment
Я взял кувалду; мой ответ может показаться вам интересным и/или пугающим. - person dfeuer; 16.09.2019
comment
Ах, я наконец-то вспомнил, почему это казалось знакомым.... RoseTree ~= CofreeT [] Maybe. Это позволяет избежать необходимости полностью индивидуального доказательства. - person dfeuer; 16.09.2019

Мы можем видеть ваш RoseTree как конкретное приложение конкретного монадного преобразователя. Давайте поместим ваше собственное определение в модуль с именем Rose и получим экземпляры Read и Show для RoseTree. Теперь мы можем пофантазировать. Примечание: вы, вероятно, еще не все поймете здесь. Некоторые из них используют довольно продвинутые языковые расширения GHC. Но я думаю, что это все равно интересно.

Мы будем использовать преобразователь cofree comonad из пакета free. Как следует из названия, он играет особую роль по отношению к классу Comonad, но оказывается, что и с Monad он может делать полезные вещи!

{-# language PatternSynonyms, ViewPatterns, GeneralizedNewtypeDeriving #-}
module FancyRose where

import Text.Read (Read (readPrec))
import qualified Rose
import Control.Comonad.Trans.Cofree
{-
newtype CofreeT f w a = CofreeT
  { runCofreeT :: w (CofreeF f a (CofreeT f w a)) }

data CofreeF f a b = a :< f b
-}

newtype RoseTree a = RoseTree { unRoseTree :: CofreeT [] Maybe a }
  deriving (Functor, Applicative, Monad, Eq, Ord)

Самое замечательное, что нам не нужно самим придумывать доказательства Applicative (или Monad) законов. Вы можете найти их все в free репозитории git. !

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

-- Create or match on an empty 'RoseTree'. This is a simple
-- bidirectional pattern synonym: writing `Nil` in an expression
-- or a pattern is just the same as writing
-- `RoseTree (CofreeT Nothing)`
pattern Nil :: RoseTree a
pattern Nil = RoseTree (CofreeT Nothing)

-- Create or match on a non-empty 'RoseTree'. This is an explicit
-- bidirectional pattern synonym. We use a view pattern to show
-- how to match on a node, and then in the `where` clause we show
-- how to construct one.
pattern Node :: a -> [RoseTree a] -> RoseTree a
pattern Node a ts <- RoseTree (CofreeT (fmap (fmap RoseTree) -> Just (a :< ts)))
  where
    Node a ts = RoseTree $ CofreeT $ Just $ a :< map unRoseTree ts

Вот как мы можем реализовать Show и Read без особых хлопот:

-- Convert a `RoseTree` to the simple representation of one.
-- Note that the pattern synonyms make this really easy!
toBasicRose :: RoseTree a -> Rose.RoseTree a
toBasicRose Nil = Rose.Nil
toBasicRose (Node a ts) = Rose.Node a (map toBasicRose ts)

-- Convert the simple representation back to a `RoseTree`.
fromBasicRose :: Rose.RoseTree a -> RoseTree a
fromBasicRose Rose.Nil = Nil
fromBasicRose (Rose.Node a ts) = Node a (map fromBasicRose ts)

instance Show a => Show (RoseTree a) where
  showsPrec p = showsPrec p . toBasicRose
instance Read a => Read (RoseTree a) where
  readPrec = fmap fromBasicRose readPrec

Все ваши тестовые случаи проходят.

Примечание о производительности

Я был обеспокоен тем, что все сопоставления могут сделать шаблон Node медленным. Но я только что проверил промежуточный язык компилятора и определил, что правила перезаписи GHC действительно срабатывают и безоговорочно избавляются от всех отображений.

person dfeuer    schedule 16.09.2019