В принципе, (>>=)
позволяет вам выполнять операции таким образом, чтобы последние операции могли вести себя по-разному на основе более ранних результатов. Более чистая функция, как вы просите, доступна в классе 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
. В этой истории многое другое, но это основы.
'БПМЖ :: Functor е => (а -> б) -> фа -> fb' – DiegoNolan
' адаптации = (. Возвращение) ' –
короткий ответ заключается в том, что '>> =' является более мощным, чем 'fmap', поэтому иногда (часто) вы * need *' >> = 'и' fmap' недостаточно хороши. –