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

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

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

Другие вопросы по тегам