Подтвердить что ты не робот

Расчетные конструкции (монады, стрелки и т.д.)

Мне стало очень интересно, как моделирование моделируется в Haskell. Несколько ресурсов описали монады как "составные вычисления", а стрелки - "абстрактные виды вычислений". Я никогда не видел моноидов, функторов или аппликативных функторов, описанных таким образом. Кажется, что им не хватает необходимой структуры.

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

Примечание: Этот вопрос аналогичен Монады против стрелок и https://stackoverflow.com/info/2395715/resources-for-learning-monads-functors-monoids-arrows-etc, но я ищу конструкции вне функций, аппликативные функторы, монады, и стрелки.

Изменить: Я признаю, что аппликативные функторы следует рассматривать как "вычислительные конструкции", но я действительно искал то, с чем я еще не сталкивался. Сюда входят аппликативные функторы, монады и стрелки.

4b9b3361

Ответ 1

Arrows обобщаются по категориям, поэтому класс Category.

 class Category f where
     (.) :: f a b -> f b c -> f a c
     id :: f a a

Определение типа Arrow имеет Category как суперкласс. Категории (в смысле haskell) обобщают функции (вы можете их компилировать, но не применять), и поэтому они определенно являются "моделью вычисления". Arrow предоставляет Category дополнительную структуру для работы с кортежами. Таким образом, в то время как Category отражает что-то о функциональном пространстве Haskell, Arrow расширяет его до типа продуктов.

Каждый Monad порождает нечто, называемое "категорией Клейсли", и эта конструкция дает вам примеры ArrowApply. Вы можете построить Monad из любого ArrowApply, чтобы полный круг не изменял ваше поведение, поэтому в некотором глубоком смысле Monad и ArrowApply - одно и то же.

 newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b }

 instance Monad m => Category (Kleisli m) where
     id = Kleisli return
     (Kleisli f) . (Kleisli g) = Kleisli (\b -> g b >>= f)

 instance Monad m => Arrow (Kleisli m) where
     arr f = Kleisli (return . f)
     first (Kleisli f) = Kleisli (\ ~(b,d) -> f b >>= \c -> return (c,d))
     second (Kleisli f) = Kleisli (\ ~(d,b) -> f b >>= \c -> return (d,c))

На самом деле каждый Arrow приводит к тому, что Applicative (универсально квантифицированный для получения прав) в дополнение к суперклассу Category, и я считаю, что сочетание соответствующих Category и Applicative достаточно для восстановления вашего Arrow.

Итак, эти структуры глубоко связаны.

Предупреждение: впереди желаемый комментарий. Одно центральное различие между мышлением Functor/Applicative/Monad и способом мышления Category/Arrow заключается в том, что while Functor и его ilk являются обобщениями на уровне объекта (типы в Haskell), Category/Arrow являются генерализацией понятия морфизма (функции в Haskell). Я убежден, что мышление на уровне обобщенного морфизма связано с более высоким уровнем абстракции, чем мышление на уровне обобщенных объектов. Иногда это хорошо, иногда это не так. С другой стороны, несмотря на то, что Arrows имеет категориальную основу, и никто из математиков не считает интересным Applicative, я понимаю, что Applicative в целом лучше понимается, чем Arrow.

В принципе вы можете думать о "категории < Arrow < ArrowApply" и "Functor < Applicative < Monad", что "Category-Functor", "Arrow-Applicative" и "ArrowApply ~ Monad".

Больше бетона ниже: Что касается других структур для моделирования вычислений: часто можно перевернуть направление "стрелок" (только значения морфизмов здесь) в категориальных построениях, чтобы получить "двойное" или "совместное построение". Итак, если монада определена как

class Functor m => Monad m where
   return :: a -> m a
   join :: m (m a) -> m a

(хорошо, я знаю, что это не так, как Haskell определяет вещи, но ma >>= f = join $ fmap f ma и join x = x >>= id, так что это может быть и так) то comonad

class Functor m => Comonad m where
   extract :: m a -> a -- this is co-return
   duplicate :: m a -> m (m a) -- this is co-join

Эта вещь оказывается довольно распространенной также. Оказывается, что Comonad - основная базовая структура клеточных автоматов. Для полноты я должен отметить, что Эдвард Кметт Control.Comonad помещает duplicate в класс между функтором и Comonad для "Расширяемые функторы", потому что вы также можете определить

   extend :: (m a -> b) -> m a -> m b -- Looks familiar? this is just the dual of >>=
   extend f = fmap f . duplicate
   --this is enough
   duplicate = extend id

Оказывается, что все Monad также являются "расширяемыми"

   monadDuplicate :: Monad m => m a -> m (m a)
   monadDuplicate = return

тогда как все Comonads являются "объединенными"

   comonadJoin :: Comonad m => m (m a) -> m a
   comonadJoin = extract

поэтому эти структуры очень близки друг к другу.

Ответ 2

Все монады - это стрелки (Монада изоморфна ArrowApply). По-другому все монады являются экземплярами Applicative, где <*> - Control.Monad.ap и *> - >>. Аппликатор слабее, потому что он не гарантирует работу >>=. Таким образом, Аппликация захватывает вычисления, которые не рассматривают предыдущие результаты и не относятся к значениям. В ретроспективе много монадического кода на самом деле является аппликативным, и с чистым переписыванием это произойдет.

Расширение монад, с недавними видами Constraint в GHC 7.4.1 теперь могут быть более приятные проекты для ограниченных монад. И есть также люди, смотрящие на параметризованные монады, и, конечно, я включаю ссылку на что-то Олег.

Ответ 3

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

Например, для реализации статических эффектов могут использоваться прикладные средства. С этим я подразумеваю эффекты, которые определены на forehand. Например, при реализации конечного автомата, отклонения или принятия состояния ввода. Они не могут использоваться для управления их внутренней структурой с точки зрения их ввода.

Тип говорит все:

 <*> :: f (a -> b) -> f a -> f b

Легко рассуждать, структура f не может зависеть от ввода a. Потому что a не может достичь f на уровне типа.

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

 >>= :: m a -> (a -> m b) -> m b

Как вы можете это видеть? Потому что a находится на том же уровне, что и m. Математически это двухэтапный процесс. Bind - это композиция из двух функций: fmap и join. Сначала мы используем fmap вместе с монадическим действием для создания новой структуры, встроенной в старую:

fmap :: (a -> b) -> m a -> m b
f :: (a -> m b)
m :: m a
fmap f :: m a -> m (m b)
fmap f m :: m (m b)

Fmap может создать новую структуру на основе входного значения. Затем мы разрушаем структуру с объединением, таким образом, мы можем манипулировать структурой изнутри монадического вычисления таким образом, который зависит от ввода:

join :: m (m a) -> m a
join (fmap f m) :: m b

Многие монады легче реализовать с помощью join:

(>>=) = join . fmap 

Это возможно с помощью монад:

addCounter :: Int -> m Int () 

Но не с помощью аппликаций, но аппликаторы (и любая монада) могут делать такие вещи, как:

addOne :: m Int ()

Стрелки больше контролируют входные и выходные типы, но для меня они действительно похожи на аппликативные. Может быть, я ошибаюсь.