Асимметрия в функции связывания

ghci> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b

Почему второй аргумент (a -> m b) вместо (m a -> m b) или даже (a -> b)? Что это концептуально о Монадах, которые требуют этой подписи? Имело бы смысл иметь классы типов с альтернативными сигнатурами t a -> (t a -> t b) -> t b resp. t a -> (a -> b) -> t b?

Ответы

Ответ 1

Более симметричным определением монады является комбинатор Клейсли, который в основном (.) для монад:

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

Он может заменить (>>=) в определении монады:

f >=> g = \a -> f a >>= g

a >>= f = const a >=> f $ ()

Ответ 2

Обычно в Haskell обычно определяется Monad в терминах return и (>>=):

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

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

class Monad m where
    fmap :: (a -> b) -> m a -> m b
    join :: m (m a) -> m a
    return :: a -> m a

Как вы можете видеть, асимметрия (>>=) была заменена асимметрией join, которая принимает m (m a) и "скручивает" два слоя m только на m a.

Вы также можете видеть, что подпись fmap соответствует вашему t a -> (a -> b) -> t b, но с измененными параметрами. Это операция, которая характеризует typeclass Functor, которая строго слабее, чем Monad: каждая монада может быть сделана функтором, но не каждый функтор может быть сделан монадой.

Что это все означает на практике? Ну, при преобразовании чего-то, что является только функтором, вы можете использовать fmap для преобразования значений "внутри" функтора, но эти значения никогда не могут влиять на "структуру" или "эффект" самого функтора. Однако с монадой это ограничение снимается.

В качестве конкретного примера, когда вы делаете fmap f [1, 2, 3], вы знаете, что независимо от того, что делает f, результирующий список будет иметь три элемента. Однако, когда вы выполняете [1, 2, 3] >>= g, для g можно преобразовать каждое из этих трех чисел в список, содержащий любое количество значений.

Аналогично, если я выполняю fmap f readLn, я знаю, что он не может выполнять какие-либо операции ввода-вывода, кроме чтения строки. Если я делаю readLn >>= g, с другой стороны, возможно, что g проверит прочитанное значение и затем использует это, чтобы решить, следует ли распечатывать сообщение или читать еще несколько строк или делать что-либо еще, что возможно в пределах IO.

Ответ 3

очень хороший ответ на этот вопрос дал Брайан Бекман в (на мой взгляд) отличном введении к монадам: Не бойтесь Монады

Вы также можете взглянуть на эту приятную главу из "Узнайте, что вы haskell": Пригоршня Монады. Это тоже очень хорошо объясняет.

Если вы хотите быть прагматичным: это должно быть так, чтобы получить функцию "делать" -языку;), но Брайан и Липовака объясняют это намного лучше (и глубже);)

PS: к вашим альтернативам: первое - это более или менее применение второго аргумента к первому. Второй альтернативой является почти fmap класс типа Functor - только с включенными аргументами (и каждая Монада является Functor - даже если Haskell type-class не ограничивает его, но он должен - но это еще одна тема;))

Ответ 4

Ну, тип (>>=) удобен для десурагирования do нотации, но несколько неестественным в противном случае.

Цель (>>=) - взять тип в монаде и функцию, которая использует аргумент этого типа для создания другого типа в монаде, а затем объединить их, подняв функцию и сглаживая дополнительный слой. Если вы посмотрите на функцию join в Control.Monad, она выполняет только шаг сглаживания, поэтому, если бы мы восприняли ее как примитивную операцию, мы могли бы написать (>>=) следующим образом:

(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
m >>= k = join (fmap k m)

Обратите внимание, однако, на обратный порядок аргументов на fmap. Причина этого становится ясна, если мы думаем о монаде Identity, которая представляет собой просто оболочку newtype вокруг простых значений. Игнорирование newtypes, fmap для Identity - это приложение-функция, а join ничего не делает, поэтому мы можем распознать (>>=) как оператор приложения, при этом аргументы перевернуты. Сравните тип этого оператора, например:

(|>) :: a -> (a -> b) -> b
x |> f = f x

Очень похожая картина. Итак, чтобы получить более четкое представление о значении типа (>>=), мы вместо этого рассмотрим (=<<), который определен в Control.Monad, который принимает свои аргументы в другом порядке. Сравнивая его с (<*>), от Control.Applicative, fmap и ($), и имея в виду, что (->) является право-ассоциативным и добавляет в лишние круглые скобки:

($)   ::                       (a ->   b) -> (  a ->   b)
fmap  :: (Functor f)     =>    (a ->   b) -> (f a -> f b)
(<*>) :: (Applicative f) =>  f (a ->   b) -> (f a -> f b)
(=<<) :: (Monad m)       =>    (a -> m b) -> (m a -> m b)

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

fmap :: (Functor f) => (a -> b) -> (f a -> f b)

Это означает, что при простой функции a -> b мы можем преобразовать ее в функцию, которая делает то же самое на типах f a и f b. Так что это просто простая трансформация, которая не может изменить или проверить структуру f, что бы это ни было.

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

Так же, как fmap, за исключением того, что он принимает тип функции, который уже находится в f. Тип функции по-прежнему не обращает внимания на структуру f, но (<*>) сам по себе должен объединить две структуры f в некотором смысле. Таким образом, это может изменять и проверять структуру, но только способом, определенным самими структурами, независимо от значений.

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

Это глубокий фундаментальный сдвиг, потому что теперь мы берем функцию, которая создает некоторую структуру m, которая объединяется со структурой, уже присутствующей в аргументе m a. Таким образом, (=<<) может не только изменить структуру, как описано выше, но функция, которая будет поднята, может создать новую структуру в зависимости от значений. Тем не менее, существует еще значительное ограничение: функция получает только одно значение и, следовательно, не может проверить общую структуру; он может только осмотреть одно место, а затем решить, какую структуру он должен разместить там.

Итак, вернитесь к своему вопросу:

Было бы целесообразно иметь классы типов с альтернативными сигнатурами t a -> (t a -> t b) -> t b resp. t a -> (a -> b) -> t b?

Если вы переписываете оба этих типа в "стандартном" порядке, как указано выше, вы можете видеть, что первый - это просто ($) со специализированным типом, а второй - fmap. Однако есть и другие варианты, которые имеют смысл! Вот несколько примеров:

contramap :: (Contravariant f) => (a -> b) -> (f b -> f a)

Это контравариантный функтор, который работает "назад". Если тип сначала кажется невозможным, подумайте о типе newtype Flipped b a = Flipped (a -> b) и о том, что вы можете с ним сделать.

(<<=) :: (Comonad w) => (w a -> b) -> (w a -> w b)

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

Ответ 5

m a -> (a -> b) -> m b - поведение Functor.fmap, что весьма полезно. Однако он более ограничен, чем >>=. Например. если вы работаете со списками, fmap может изменить эти элементы и их типы, но не длину списка. С другой стороны, >>= может сделать это легко:

[1,2,3,4,5] >>= (\x -> replicate x x)
-- [1,2,2,3,3,3,4,4,4,4,5,5,5,5,5]

m a -> (m a -> m b) -> m b не очень интересно. Это просто функциональное приложение (или $) с обратными аргументами: у меня есть функция m a -> m b и укажите аргумент m a, затем я получаю m b.

[изменить]

Странно, никто не упомянул о четвертой возможной сигнатуре: m a -> (m a -> b) -> m b. Это действительно имеет смысл и приводит к Comonads

Ответ 6

Я попытаюсь ответить на это, обратившись назад.

Введение

На базовом уровне у нас есть значения: вещи с типами Int, Char, String * и т.д. Обычно они имеют полиморфный тип a, который является только переменной типа.

Иногда полезно иметь значение в контексте. Следуя блогу sigfpe, мне нравится думать об этом как о фантастическом значении. Например, если у нас есть что-то, что может быть Int, но может быть не что-либо, оно в контексте Maybe. Если что-то есть либо Int, либо String, оно находится в контексте Either String. Если значение, возможно, является одним из нескольких разностей Char s, оно находится в контексте индетерминизма, который в haskell является списком, т.е. [Char].

(несколько продвинутый: новый контекст вводится с конструктором типа, который имеет вид * -> *).

функторы

Если у вас есть причудливое значение (значение в контексте), было бы неплохо применить к нему функцию. Конечно, вы можете написать конкретные функции для этого для каждого другого контекста (Maybe, Either n, Reader, IO и т.д.), Но мы хотели бы использовать один и тот же интерфейс во всех этих случаях. Это обеспечивается классом типа Functor.

Метод только для метода fmap, который имеет тип (a -> b) -> f a -> f b. Это означает, что если у вас есть функция от типа a до типа b, вы можете применить ее к фантазии a, чтобы получить фантазию b, где b выглядит так же, как и a.

g' = fmap (+1) (g :: Maybe Int)          -- result :: Maybe Int

h' = fmap (+1) (h :: Either String Int)  -- result :: Either String Int

i' = fmap (+1) (i :: IO Int)             -- result :: IO Int

Здесь g', h' и i' имеют точно такие же контексты, как g, h и i. Контекст не изменяется, а только значение внутри него.

(Следующий шаг Applicative, который я сейчас пропущу).

Монады

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

safe2Div :: Int -> Maybe Int
safe2Div 0 = Nothing
safe2Div n = Just (2 `div` n)

Как вы примените это к Maybe Int? Вы не можете использовать fmap, потому что

fmap safe2Div (Just 0) :: Maybe (Maybe Int)

который выглядит еще сложнее. * Вам нужна функция Maybe Int -> (Int -> Maybe Int) -> Maybe Int

Или, может быть, это:

printIfZ :: Char -> IO ()
printIfZ 'z' = putStrLn "z"
printIfZ _   = return ()

Как вы можете применить это к IO Char? Опять же, вы хотите, чтобы функция IO Char -> (Char -> IO ()) -> IO () выполняла соответствующее действие ввода-вывода в зависимости от значения.

Как правило, это дает вам подпись типа

branchContext :: f a -> (a -> f b) -> f b

что является в точности возможностью, предоставляемой методом Monad (>>=).

Я бы порекомендовал Typeclassopedia для получения дополнительной информации об этом.

Изменить: для t a -> (t a -> t b) -> t b для этого нет необходимости в классе типа, поскольку он просто перевернул приложение-приложение, т.е. flip ($). Это связано с тем, что он вообще не зависит от структуры контекста или внутреннего значения.

* - игнорировать, что String является синонимом типа [Char]. Это все равно значение независимо.

* - он выглядит более сложным, но оказывается, что (>>=) :: m a -> (a -> m b) -> m b и join :: m (m a) -> m a дают вам точно такую ​​же мощность. (>>=) обычно более полезен на практике.

Ответ 7

Что это концептуально о Monads, для которого требуется эта подпись?

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

Подпись типа "привязка" m a -> (a -> m b) -> m b в основном говорит: "У меня есть этот a, но он застрял в Monad m. И у меня есть эта монадическая функция, которая приведет меня от a до m b Я не могу просто применить a к этой функции, хотя, поскольку у меня нет только a, это a m a. Поэтому придумайте функцию вроде как $ и вызовите it >>=. Все, что является Монадой, в основном должно сказать мне (определить), как развернуть a из m a, чтобы я мог использовать эту функцию a -> m b на нем."

Ответ 8

Каждая монада связана с некоторым "присоединением", которое представляет собой пару карт, которые являются своего рода частичными обратными друг к другу. Например, рассмотрим пару "goInside" и "goOutside". Вы начинаете внутри, а затем выходите. Вы сейчас снаружи. Если вы входите внутрь, вы снова окажетесь внутри.

Обратите внимание, что как внутри, так и снаружи связаны друг с другом - этой парой функций, которые отображают объект или лицо взад и вперед.

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

Это позволяет нам переключаться между двумя контекстами по своему усмотрению - "чистый" (я использую это в неопределенном, наводящем смысле) контексте вне монады и монадический контекст внутри него.