Русский
Русский
English
Статистика
Реклама

Traversable

Пытаясь композировать некомпозируемое монады

04.01.2021 22:21:38 | Автор: admin

Сколько раз вы слышали эту мантру "монады не композируются"? Я потратил достаточно много времени, чтобы попробовать опровергнуть это утверждение, пытаясь решить проблему в лоб. Но как и многие вещи в математике, порой, чтобы попробовать что-то понять, иногда стоит сменить масштаб.

Рекомендуется прочитать первую и вторую части этой серии, если вы еще этого не сделали.

Когда мы хотим слить два эффекта в один, то есть сцепить их в трансформер, у нас есть два варианта: вложить левый в правый, либо правый в левый. Эти два варианты определены со схемами TU и UT:

newtype TU t u a = TU (t :. u := a)newtype UT t u a = UT (u :. t := a)

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

type instance Schema (Reader e) = TU ((->) e)type instance Schema (Either e) = UT (Either e)type instance Schema Maybe = UT Maybe

Экземпляры обычного ковариантного и аппликативного функтора выглядят тривиально, так как это все еще функтор, а функторы композируются:

(<$$>) :: (Functor t, Functor u) => (a -> b) -> t :. u := a -> t :. u := b(<$$>) = (<$>) . (<$>)(<**>) :: (Applicative t, Applicative u) => t :. u := (a -> b) -> t :. u := a -> t :. u := bf <**> x = (<*>) <$> f <*> xinstance (Functor t, Functor u) => Functor (TU t u) where    fmap f (TU x) = TU $ f <$$> xinstance (Applicative t, Applicative u) => Applicative (TU t u) where    pure = TU . pure . pure    TU f <*> TU x = TU $ f <**> xinstance (Functor t, Functor u) => Functor (UT t u) where    fmap f (UT x) = UT $ f <$$> xinstance (Applicative t, Applicative u) => Applicative (UT t u) where    pure = UT . pure . pure    UT f <*> UT x = UT $ f <**> x

Проблемы возникают, если мы попробуем описать монады. Непонятно, как найти обобщенный способ, учитывая, что оба эффекта нам неизвестны:

instance (Monad t, Monad u) => Monad (TU t u) where  x >>= f = ???instance (Monad t, Monad u) => Monad (UT t u) where  x >>= f = ???

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

instance Monad u => Monad (TU ((->) e) u) where    TU x >>= f = TU $ \e -> x e >>= ($ e) . run . finstance Monad u => Monad (UT (Either e) u) where    UT x >>= f = UT $ x >>= \case        Left e -> pure $ Left e        Right r -> run $ f rinstance Monad u => Monad (UT Maybe u) where    UT x >>= f = UT $ x >>= \case        Nothing -> pure Nothing        Just r -> run $ f r

В случае обработки ошибок (Maybe и Either), мы можем увидеть похожее поведение: если инвариант содержит параметр a, тогда цепочка вычислений продолжается. Это невероятно похоже на Traversable! Вот так он выглядит:

class (Functor t, Foldable t) => Traversable t where    traverse :: Applicative f => (a -> f b) -> t a -> f (t b)instance Traversable Maybe where    traverse _ Nothing = pure Nothing    traverse f (Just x) = Just <$> f xinstance Traversable (Either a) where    traverse _ (Left x) = pure (Left x)    traverse f (Right y) = Right <$> f y

Давайте попробуем его использовать:

instance (Traversable t, Monad t, Monad u) => Monad (UT t u) where    UT x >>= f = UT $ x >>= \i -> join <$> traverse (run . f) i

Работает! То есть, мы можем применять связывание для трансформера с известным нам Traversable эффектом.

А что нам делать с TU, для которого подходит эффект неизменяемого окружения - Reader? Я правда не знаю. Но мы можем кое-что попробовать - возьмём класс антипод Traversable - Distributive. И какое счастье, для него может быть определен Reader (точнее, его внутренняя часть - (->) e)!

class Functor g => Distributive g where    collect :: Functor f => (a -> g b) -> f a -> g (f b)instance Distributive ((->) e) where    collect f q e = flip f e <$> q

Но почему именно эти два класса функторов и почему они антиподы по отношению друг к другу? Понять это помогут модификации их методов, где вместо функций a -> t b мы подставляем функцию, которая ничего не меняет - id:

sequence :: (Traversable t, Applicative u) => t (u a) -> u (t a)sequence = traverse iddistribute :: (Distributive t, Functor u) => u (t a) -> t (u a)distribute = collect id

Вот оно! Мы можем увидеть, что эти методы позволяют нам менять порядок эффектов. Раз уж сработал Traversable для обратной композиции, может Distributive подойдет для прямой?

instance (Monad t, Distributive t, Monad u) => Monad (TU t u) where    TU x >>= f = TU $ x >>= \i -> join <$> collect (run . f) i

Тоже работает! Значит у нас уже есть определенный алгоритм, как определять монады для таких трансформеров:

  • Обратная схема UT - полагайся на Traversable.

  • Прямая схема TU - полагайся на Distributive.

Но у нас есть также схема посложнее, которая используется в монаде State и комонаде Store:

newtype TUT t t' u a = TUT (t :. u :. t' := a)newtype State s a = State ((->) s :. (,) s := a)newtype Store s a = Store ((,) s :. (->) s := a)type instance Schema (State s) = TUT ((->) s) ((,) s)type instance Schema (Store s) = TUT ((,) s) ((->) s)

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

instance (Functor t, Functor t', Functor u) => Functor (TUT t t' u) where    fmap f (TUT x) = TUT $ f <$$$> x

Так, стрелка ( (->) s) является Distributive, а запятая ((,) s) - Traversable... Но между этими двумя эффектами также существует связь посильнее, которая называется сопряжением (подробнее можно почитать здесь):

class Functor t => Adjunction t u where    leftAdjunct  :: (t a -> b) -> a -> u b    rightAdjunct :: (a -> u b) -> t a -> b    unit :: a -> u :. t := a    unit = leftAdjunct id    counit :: t :. u := a -> a    counit = rightAdjunct idinstance Adjunction ((,) s) ((->) s) where    leftAdjunct :: ((s, a) -> b) -> a -> (s -> b)     leftAdjunct f a s = f (s, a)    rightAdjunct :: (a -> s -> b) -> (s, a) -> b    rightAdjunct f (s, a) = f a s    unit :: a -> s -> (s, a)    unit x = \s -> (s, x)    counit :: (s, (s -> a)) -> a    counit (s, f) = f s

Попробуем сначала разобраться с эффектом состояния. Погружение в эффект State полностью идентичен методу unit, а сопряжение справа используется, чтобы связать два эффекта в монадной цепочке:

instance Monad (State s) where    State x >>= f = State $ rightAdjunct (run . f) <$> x    -- Или так: State x >>= f = State $ counit <$> ((run . f) <$$> x)    return = State . unit

А как быть в случае с трансформером? Тут у нас меж двух функторов ((->) s) и ((,) s) есть произвольный эффект, который надо учитывать. Для погружения в трансформер нам понадобится сопряжение слева, а для связывания - сопряжение справа:

instance (Adjunction t' t, Monad u) => Monad (TUT t t' u) where    x >>= f = TUT $ (>>= rightAdjunct (run . f)) <$> run x    return = TUT . (leftAdjunct pure)

Поразительно, но для того чтобы описать экземпляр комонады для этой же схемы, нам нужно сделать все в точности наоборот:

instance (Adjunction t' t, Comonad u) => Comonad (TUT t' t := u) where    extend f x = TUT $ (=>> leftAdjunct (f . TUT)) <$> run x    extract = rightAdjunct extract . run

А что, если нам нужно не только погружать значение в трансформер, но еще и поднимать произвольный эффект до уровня трансформера? И тут нам понадобятся сопряжения! Причем, для монадных и комонадных трансформеров результаты невероятно симметричны...

instance (Adjunction t' t, Distributive t) => MonadTrans (TUT t t') where    lift = TUT . collect (leftAdjunct id)instance (Adjunction t' t, Applicative t, forall u . Traversable u) => ComonadTrans (TUT t' t) where    lower = rightAdjunct (traverse id) . run

Из этого всего, мы можем сделать выводы:

  • Если эффект имеет экземпляр Traversable - подходит обратная схема UT.

  • Если эффект имеет экземпляр Distributive - подходит прямая схема TU.

  • Если компоненты эффекта образуют сопряжение (Adjunction) - подходит схема TUT.

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

Исходники с определениями можно найтиздесь. Примеры применения описанной системы эффектов можно посмотреть тут.

Подробнее..

Перевозим волка, козу и капусту через реку с эффектами на Haskell

02.08.2020 16:06:23 | Автор: admin
Однажды крестьянину понадобилось перевезти через реку волка, козу и капусту. У крестьянина есть лодка, в которой может поместиться, кроме самого крестьянина, только один объект или волк, или коза, или капуста. Если крестьянин оставит без присмотра волка с козой, то волк съест козу; если крестьянин оставит без присмотра козу с капустой, коза съест капусту.




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



Начнем с самого простого маршрута перемещений. Так как мы не знаем заранее, через какое гарантированное количество шагов мы получим решение, можно построить бесконечный маршрут, все равно мы будем вычислять его лениво:

data Direction = Back | Forwardroute :: [Direction]route = iterate alter Forwardalter :: Direction -> Directionalter Back = Forwardalter Forward = Back


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

data Character = Wolf | Goat | Cabbage deriving Eqclass Survivable a wheresurvive :: a -> a -> Orderinginstance Survivable Character wheresurvive Wolf Goat = GTsurvive Goat Wolf = LTsurvive Goat Cabbage = GTsurvive Cabbage Goat = LTsurvive _ _ = EQ


Зачем вообще использовать эффекты? Эффекты помогают бороться со сложностью, которая присуща любой предметной области. Значит, для того, чтобы определить какие эффекты использовать для решения головоломки, стоит подумать над тем, с какими сложностями мы можем столкнуться, когда попробуем описать решение задачи с помощью кода:

  • Чтобы найти решение, при котором все персонажи будут перевезены на противоположный берег, надо перебрать много вариантов перестановок. Для этого мы будем использовать эффект множественности, которого можно добиться с помощью обычного списка.
  • Еще нам нужно запоминать местоположение персонажа, чтобы проверять условия совместимости с другими персонажами (волк ест козу, коза ест капусту) и кого можно посадить на лодку. Мы можем хранить состав двух берегов type River a = ([a],[a]) c помощью эффекта состояния State (River a).
  • Лодка может взять кого-нибудь на борт, а может и не брать тут нам пригодится эффект частичности с Maybe.


В коде я буду использовать свою экспериментальную библиотеку joint (на Хабре есть две статьи, объясняющие ее суть первая и вторая), но при желании решение можно перенести на transformers или mtl.

Итак, у нас есть три разрозненных эффекта: состояние, множественность, частичность. Теперь надо решить, как мы собираемся их скомпоновать между собой:

  • В аппликативной/монадной цепочке вычислений для Maybe, если мы где-то получили Nothing, то и результат всего вычислений будет Nothing. Мы оставим его отдельно, так как не хотим, чтобы при отправлении пустой лодки (без персонажа, крестьянина мы не учитываем) мы потеряли весь прогресс в нахождении решения.
  • Каждый последующий выбор хода (эффект множественности) должен опираться на состав текущего берега (эффект состояния), так как мы не можем взять персонажа в лодку, если она находится на другом берегу. Следовательно, нам нужно эти эффекты сцепить в трансформер: State (River a) :> [].


Один ход в головоломке можно описать как последовательность действий:

  1. Получить состав персонажей на текущем берегу
  2. Выбрать следующего персонажа для транспортировки
  3. Переместить персонажа на противоположный берег


step direction = bank >>= next >>= transport


Давайте пройдемся по каждому шагу подробнее.

В зависимости от направления перемещения лодки, применяем линзу для источника отправления к состоянию всей реки и получаем состав текущего берега:

bank :: (Functor t, Stateful (River a) t) => t [a]bank = view (source direction) <$> current


Выбор следующего персонажа происходит так: получая набор персонажей с берега (предыдущее выражение bank), мы формируем пространство выбора, добавляя к этому самому пространству пустую лодку:

\xs -> Nothing : (Just <$> xs) 


Для каждого кандидата (пустая лодка (Nothing) тоже кандидат) проверяем чтобы на оставшемся берегу не оставалось персонажей, которые были бы не прочь полакомиться друг другом:

valid :: Maybe a -> Boolvalid Nothing = and $ coexist <$> xs <*> xsvalid (Just x) = and $ coexist <$> delete x xs <*> delete x xscoexist :: Survivable a => a -> a -> Boolcoexist x y = survive x y == EQ


И когда мы отфильтровали пространство выбора персонажей, поднимаем эффект множественности и возвращаем каждый элемент из этого пространства выбора:

next :: (Survivable a, Iterable t) => [a] -> t (Maybe a)next xs = lift . filter valid $ Nothing : (Just <$> xs)


Остался последний шаг фактическая транспортировка c помощью линз: удаляем персонажа с берега отправки и добавляем к берегу назначения:

leave, land :: River a -> River aleave = source direction %~ delete xland = target direction %~ (x :)


Если в лодке был персонаж изменяем состояние реки, иначе ход был холостым:

transport :: (Eq a, Applicative t, Stateful (River a) t) => Maybe a -> t (Maybe a)transport (Just x) = modify @(River a) (leave . land) $> Just x wheretransport Nothing = pure Nothing


Было бы неплохо посмотреть на работу программы в действии. Для нахождения решения нам нужно как минимум совершить семь шагов по маршруту:

start :: River Characterstart = ([Goat, Wolf, Cabbage], [])solutions = run (traverse step $ take 7 route) start


И у нас есть два решения:



Полные исходник можно посмотреть здесь.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru