Асимметрия в функции связывания - PullRequest
11 голосов
/ 06 сентября 2011
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 ]

16 голосов
/ 06 сентября 2011

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

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

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

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

a >>= f = const a >=> f $ ()
12 голосов
/ 06 сентября 2011

Обычно в Хаскеле определяют 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.

11 голосов
/ 06 сентября 2011

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

У вас также может бытьпосмотрите на эту прекрасную главу из «Изучите свой хаскель»: Горсть монад .Это тоже объясняет это очень хорошо.

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

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

10 голосов
/ 06 сентября 2011

Ну, тип (>>=) удобен для десагерирования нотации 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 мы можем преобразовать ее в функцию, которая делает то же самое с типами * 1039.* и 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)

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

5 голосов
/ 06 сентября 2011

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.

[Edit]

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

4 голосов
/ 06 сентября 2011

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

Введение

На базовом уровне у нас есть значения : вещи с такими типами, как 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 , вы можете применить ее к fancy a , чтобы получить fancy 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 дают вам точно такую ​​же силу. (>>=) обычно более полезен на практике.

3 голосов
/ 06 сентября 2011

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

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

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

0 голосов
/ 15 сентября 2011

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

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

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

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

...