Асимметрия в функции связывания
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 - это функция, которая принимает значение "внутри" монады, помещает ее в контекст вне монады, функцию в монаду, а затем возвращает значение внутри монады, так что вы всегда уверены чтобы быть в правильном месте для продолжения операций.
Это позволяет нам переключаться между двумя контекстами по своему усмотрению - "чистый" (я использую это в неопределенном, наводящем смысле) контексте вне монады и монадический контекст внутри него.