Асимметрия в функции связывания
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
соответственно t a -> (a -> b) -> t b
?
8 ответов
Более симметричным определением монады является комбинатор Клейсли, который в основном (.)
для монад:
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
Может заменить (>>=)
в определении монады:
f >=> g = \a -> f a >>= g
a >>= f = const a >=> f $ ()
Обычно в Хаскеле определяют 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
, но с обратными параметрами. Это операция, которая характеризует класс типов Functor
что строго слабее чем Monad
: каждую монаду можно сделать функтором, но не каждый функтор можно сделать монадой.
Что это все означает на практике? Что ж, при преобразовании чего-то, что является только функтором, вы можете использовать fmap
для преобразования значений "внутри" функтора, но эти значения никогда не могут влиять на "структуру" или "эффект" самого функтора. Однако с монадой это ограничение снято.
В качестве конкретного примера, когда вы делаете fmap f [1, 2, 3]
Знаете, что несмотря ни на что f
делает, результирующий список будет иметь три элемента. Тем не менее, когда вы делаете [1, 2, 3] >>= g
, это возможно для g
преобразовать каждое из этих трех чисел в список, содержащий любое количество значений.
Точно так же, если я делаю fmap f readLn
Я знаю, что он не может выполнять какие-либо действия ввода-вывода, кроме чтения строки. Если я сделаю readLn >>= g
с другой стороны, это возможно для g
проверить прочитанное значение и затем использовать его, чтобы решить, следует ли распечатать сообщение или прочитать еще n строк, или сделать что-либо еще, что возможно в пределах IO
,
Очень хороший ответ на этот вопрос дал Брайан Бекман в (на мой взгляд) отличном введении в монады: не бойтесь монады
Вы также можете взглянуть на эту прекрасную главу из "Изучите свой хаскель": "Горсть монад". Это тоже очень хорошо объясняет.
Если вы хотите быть прагматичным: это должно быть так, чтобы запустить функцию "до" - язык;) - но Брайан и Липовака объясняют это намного лучше (и глубже), чем это;)
PS: к вашим альтернативам: первый - более или менее применение второго аргумента к первому. Второй вариант почти fmap
класса Functor-type - только с переключаемыми аргументами (и каждая Monad является Functor - даже если класс типа Haskell не ограничивает его, но должен - но это другая тема;))
Ну типа (>>=)
удобно для обессиления do
нотации, но несколько неестественно в противном случае.
Цель (>>=)
это взять тип в монаде и функцию, которая использует аргумент этого типа для создания какого-либо другого типа в монаде, а затем объединить их, подняв функцию и сгладив дополнительный слой. Если вы посмотрите на join
функция в Control.Monad
, он выполняет только этап выравнивания, поэтому, если бы мы взяли его в качестве примитивной операции, мы могли бы написать (>>=)
как так:
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b
m >>= k = join (fmap k m)
Обратите внимание, однако, обратный порядок аргументов fmap
, Причина этого становится ясной, если мы подумаем о Identity
монада, которая является просто оболочкой нового типа для простых значений. Игнорирование новых типов, 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
соответственно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)
Это двойственность монады - тогда как аргумент (=<<)
может только осмотреть местную область и создать часть структуры, чтобы поставить там, аргумент (<<=)
может проверить глобальную структуру и вывести итоговое значение. (<<=)
сам по себе обычно просматривает структуру в некотором смысле, принимая суммарное значение с каждой точки зрения, а затем повторно собирает их для создания новой структуры.
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
Я попытаюсь ответить на это, работая задом наперед.
Вступление
На базовом уровне у нас есть ценности: вещи с такими типами, как 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
"s (>>=)
метод.
Я бы порекомендовал 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
дать вам точно такую же силу. (>>=)
обычно более полезен на практике.
Что концептуально в Монаде требует этой подписи?
В основном все. Монады все об этой особой типовой сигнатуре, по крайней мере, с одного взгляда на них.
Подпись типа "связать" m a -> (a -> m b) -> m b
в основном говорит: "У меня есть это a
застряла в монаде m
, И у меня есть эта монадическая функция, которая возьмет меня от a
в m b
, Я не могу просто применить a
к этой функции, хотя, потому что у меня нет только a
, это m a
, Итак, давайте изобретем функцию вроде $
и назовите это >>=
, Все, что является Монадой, в основном должно сказать мне (определить), как развернуть a
от m a
так что я могу использовать эту функцию a -> m b
в теме."
Каждая монада связана с неким "присоединением", представляющим собой пару карт, которые являются своего рода частичными инверсиями друг к другу. Например, рассмотрим пару "goInside" и "goOutside". Вы начинаете внутри, а затем идете наружу. Вы сейчас снаружи. Если вы идете внутрь, вы в конечном итоге обратно внутрь.
Обратите внимание на то, как нахождение внутри и снаружи связано с этой парой функций, которые отображают объект или человека назад и вперед.
Bind - это функция, которая принимает значение "внутри" монады, помещает его в контекст вне монады, функцию в монаду, а затем возвращает значение обратно в монаду, так что вы всегда уверены, что находитесь в правильное начальное место для продолжения операций.
Это позволяет нам по желанию переключаться между двумя контекстами - "чистым" (я использую это в неопределенном, наводящем на мысль смысле) контекстом вне монады и монадическим контекстом внутри нее.