2014-01-19 4 views
7

Это подпись хорошо знаю оператор >> = в HaskellНа подпись >> = оператора Монада

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

Вопрос заключается в том, почему тип функции

(a -> m b) 

вместо от

(a -> b) 

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

, наоборот, кажется, не трудно написать общий «адаптер»

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

, но в любом случае я считаю более вероятным, что у вас уже есть (a -> b) вместо (a -> m b).

Примечание. Я объясняю, что я имею в виду под «пратическими» и «вероятными». Если вы еще не определили какую-либо монаду в программе, тогда у вас есть «чистые» (a -> b), и у вас будет 0 функций типа (a -> m b) только потому, что вы еще не определили m. Если тогда вы решите определить монаду m, то возникает необходимость в определении новых функций a -> m b.

+6

'БПМЖ :: Functor е => (а -> б) -> фа -> fb' – DiegoNolan

+4

' адаптации = (. Возвращение) ' –

+1

короткий ответ заключается в том, что '>> =' является более мощным, чем 'fmap', поэтому иногда (часто) вы * need *' >> = 'и' fmap' недостаточно хороши. –

ответ

7

В принципе, (>>=) позволяет вам выполнять операции таким образом, чтобы последние операции могли вести себя по-разному на основе более ранних результатов. Более чистая функция, как вы просите, доступна в классе Functor и выводится с использованием (>>=), но если вы застряли в ней, вы больше не сможете выполнять операции. Существует также промежуточный элемент, называемый Applicative, который позволяет выполнять последовательности операций, но не изменять их на основе промежуточных результатов.

В качестве примера давайте создадим простой тип действия ввода-вывода от Functor to Applicative to Monad.


Мы сосредоточимся на тип GetC, который следующим образом

GetC a = Pure a | GetC (Char -> GetC a) 

Первый конструктор будет иметь смысл во времени, но второй должен иметь смысл immediately- GetC имеет функцию, которая может реагировать на входящий символ. Мы можем превратить GetC в IO действия для того, чтобы обеспечить эти символы

io :: GetC a -> IO a 
io (Pure a) = return a 
io (GetC go) = getChar >>= (\char -> io (go char)) 

Что делает его ясно, где Pure приходит от --- он обрабатывает чистые значения нашего типа. Наконец, мы собираемся сделать GetC аннотация: мы собираемся запретить использование Pure или GetC и разрешить нашим пользователям доступ только к тем функциям, которые мы определяем.Я напишу самый важный сейчас

getc :: GetC Char 
getc = GetC Pure 

Функция, которая сразу получает символ, является чистым значением. Хотя я назвал это самой важной функцией, ясно, что сейчас GetC довольно бесполезен. Все, что мы можем сделать, это запустить getc, а затем io ... чтобы получить эффект, полностью эквивалентный getChar!

io getc  ===  getChar  :: IO Char 

Но мы будем строить отсюда.


Как указано в начале, Functor класс типов обеспечивает функцию так же, как вы ищете называется fmap.

class Functor f where 
    fmap :: (a -> b) -> f a -> f b 

Оказывается, что мы можем создать экземпляр GetC как Functor так давайте сделаем это.

instance Functor GetC where 
    fmap f (Pure a) = Pure (f a) 
    fmap f (GetC go) = GetC (\char -> fmap f (go char)) 

Если вы косоглазие, вы заметите, что fmap влияет только на Pure конструктор. В конструкторе GetC он просто «отжимается» и откладывается дольше. Это намек на слабость fmap, но давайте попробуем.

io      getc :: IO Char 
io (fmap ord    getc) :: IO Int 
io (fmap (\c -> ord + 1) getc) :: IO Int 

Мы получили возможность изменять тип возвращаемого нашей IO интерпретации нашего типа, но именно об этом! В частности, мы по-прежнему ограничиваемся получением одного символа, а затем возвращаемся к IO, чтобы сделать с ним что-нибудь интересное.

Это недостаток Functor. Поскольку, как вы отметили, он имеет дело только с чистыми функциями, он застревает «в конце вычисления», изменяя только конструктор Pure.


Следующий шаг Applicative который простирается Functor как этот

class Functor f => Applicative f where 
    pure :: a -> f a 
    (<*>) :: f (a -> b) -> f a -> f b 

Другими словами, он расширяет понятие инъекционного чистых ценностей в нашем контексте и позволяя чистая функция приложения, чтобы пересечь тип данных. Неудивительно, что GetC конкретизирует Applicative тоже

instance Applicative GetC where 
    pure = Pure 
    Pure f <*> Pure x = Pure (f x) 
    GetC gof <*> getcx = GetC (\char -> gof <*> getcx) 
    Pure f <*> GetC gox = GetC (\char -> fmap f (gox char)) 

Аппликативные позволяют нам последовательность операций и которые могут быть ясны из определения уже. На самом деле, мы видим, что (<*>) нажимает символьное приложение вперед, чтобы действия GetC по обе стороны от (<*>) выполнялись в порядке. Мы используем Applicative как этот

fmap (,) getc <*> getc :: GetC (Char, Char) 

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

getAll :: GetC [Char] 
getAll = fmap (:) getc <*> getAll 

который демонстрирует характер Applicative будучи в состоянии последовательности действия одного за другим.

Проблема в том, что мы не можем остановиться. io getAll - бесконечный цикл, потому что он просто потребляет персонажей навсегда. Мы не можем сказать, чтобы он остановился, когда он видит '\n', например, потому что Applicative s последовательность, не замечая более ранних результатов.


Так давайте заключительный шаг в Instantiate Monad

instance Monad GetC where 
    return = pure 
    Pure a >>= f = f a 
    GetC go >>= f = GetC (\char -> go char >>= f) 

что позволяет немедленно осуществить тормозное getAll

getLn :: GetC String 
getLn = getc >>= \c -> case c of 
    '\n' -> return [] 
    s -> fmap (s:) getLn 

Или, используя do обозначения

getLn :: GetC String 
getLn = do 
    c <- getc 
    case c of 
    '\n' -> return [] 
    s -> fmap (s:) getLn 

Так что же дает? Почему мы можем написать цикл остановки?

Поскольку (>>=) :: m a -> (a -> m b) -> m b позволяет второй аргумент функции чистого значения, выбрать следующее действие, m b. В этом случае, если входящий символ равен '\n', мы выбираем return [] и завершаем цикл. Если нет, мы рекурсируем.

Именно поэтому вы можете захотеть Monad по телефону Functor. В этой истории многое другое, но это основы.

9

Причина в том, что (>>=) является более общим. Функция вы предлагаете называется liftM и легко может быть определена как

liftM :: (Monad m) => (a -> b) -> (m a -> m b) 
liftM f k = k >>= return . f 

Это понятие имеет свой собственный класс типа называется Functor с fmap :: (Functor m) => (a -> b) -> (m a -> m b). Каждый Monad также является Functor с fmap = liftM, но по историческим причинам это не (yet), захваченное в иерархии типа.

И adapt вы предлагаете может быть определена как

adapt :: (Monad m) => (a -> b) -> (a -> m b) 
adapt f = return . f 

Обратите внимание, что наличие adapt эквивалентно наличию returnreturn, как может быть определена как adapt id.

Так что все, что имеет >>=, может также иметь эти две функции, но не наоборот. There are structures that are Functors but not Monads.

Интуиция, лежащая в основе этой разницы, проста: вычисление в монаде может зависеть от результатов предыдущих монадов. Важным элементом является (a -> m b), что означает, что не только b, но и его «эффект» m b может зависеть от a. Например, мы можем иметь

import Control.Monad 

mIfThenElse :: (Monad m) => m Bool -> m a -> m a -> m a 
mIfThenElse p t f = p >>= \x -> if x then t else f 

, но это не представляется возможным определить эту функцию только с Functor m ограничения, используя только fmap. Функторы позволяют нам изменять значение «внутри», но мы не можем принять его «вне», чтобы решить, какое действие предпринять.

1

Как и другие, ваша связь является функцией fmap класса Functor, a.k.a <$>.

Но почему он менее эффективен, чем >>=?

, кажется, не трудно написать общий «адаптер»

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

Вы действительно можете написать функцию с этим типом:

adapt f x = return (f x) 

Однако эта функция не может сделать все возможное, чтобы сделать аргумент >>=. Есть полезные значения, которые adapt не может произвести.

В списке monad, return x = [x], поэтому adapt всегда будет возвращать список из одного элемента.

В Maybe монада, return x = Some x, поэтому adapt никогда не вернется None.

В монаде IO, как только вы получили результат операции, все, что вы можете сделать, это вычислить из нее новое значение, вы не сможете выполнить последующую операцию!

и т. Д. Итак, fmap способен делать меньше, чем >>=. Это не значит, что это бесполезно - у него не было бы имени, если бы это было :) Но он менее мощный.

0

Вся «точка» монады действительно (что делает ее выше функтора или аппликативного) состоит в том, что вы можете определить монаду, которую вы «возвращаете» на основе значений/результатов левой стороны.

Например, >>= на Maybe типа позволяет принять решение о возвращении Just x или Nothing. Вы заметите, что, используя функторы или аппликативные, невозможно «выбрать», чтобы вернуть Just x или Nothing на основе «упорядоченного». Может быть.

Try реализации что-то вроде:

halve :: Int -> Maybe Int 
halve n | even n = Just (n `div` 2) 
     | otherwise = Nothing 

return 24 >>= halve >>= halve >>= halve 

только с <$> (fmap1) или <*> (ap).

На самом деле «простая интеграция чистого кода», о которой вы упоминаете, является важным аспектом functor design pattern и очень полезна. Однако это во многом не связано с мотивацией, стоящей за >>= --- они предназначены для разных приложений и вещей.

-1

Я думаю, что точки ответа Дж ABRAHAMSON в к правильной причине:

В принципе, (>> =) позволяет последовательно ОПЕРАЦИЯМ таким образом, что последние операции могут выбрать вести себя по-разному на основе более ранние результаты.Более чистая функция, как вы просите, доступна в классе Functor и выводится с использованием (>> =), но если вы застряли с ней в одиночку , вы больше не сможете выполнять операции на всех.

И позвольте мне показать простой контрпример от >>= :: Monad m => m a -> (a -> b) -> m b.

Понятно, что мы хотим иметь значения, связанные с контекстом. И, возможно, нам понадобится последовательно цепочка функций над такими «контекстными значениями». (Это всего лишь один вариант использования для Monads).

Возьмите Maybe просто как случай «контекстного значения».

Затем определяют "фальшивый" класс монады:

class Mokad m where 
    returk :: t -> m t 
    (>>==) :: m t1 -> (t1 -> t2) -> m t2 

Теперь давайте попробуем иметь Maybe быть экземпляр Mokad

instance Mokad Maybe where 
     returk x = Just x 
     Nothing >>== f = Nothing 
     Just x >>== f = Just (f x) -- ????? always Just ????? 

Появится первая проблема: >>== всегда возвращается Just _.

Теперь давайте попробуем функцию цепи над Maybe использованием >>== (мы последовательно извлечь значения из трех Maybe с просто добавить их)

chainK :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int 
chainK ma mb mc = md 
     where 
     md = ma >>== \a -> mb >>== \b -> mc >>== \c -> returk $ a+b+c 

Но этот код не компилируется: md типа Maybe (Maybe (Maybe Int)) потому что каждый раз, когда используется >>==, он инкапсулирует предыдущий результат в поле Maybe.

И наоборот >>= работает отлично:

chainOK :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int 
chainOK ma mb mc = md 
     where 
     md = ma >>= \a -> mb >>= \b -> mc >>= \c -> return (a+b+c) 
+0

Означает ли нижестоящий знак, что Дж. Абрахамсон ошибается? Или что мой пример неправильный? – cibercitizen1

Смежные вопросы