2017-02-15 30 views
5

Играя с Haskell, и теперь я пытаюсь создать функцию, какHaskell: Запуск два монад, сохранить результат первого одного

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

со следующими семантическим: оно должно применяться значением монады к функции, возвращающей вторая монада и держать результат первой монаду, но эффекта второго одного

у меня есть рабочая функция в случае Maybe монады:

keepValueMaybe :: Maybe a -> (a -> Maybe b) -> Maybe a 

keepValue ma f = case ma >>= f of 
    Nothing -> Nothing 
    Just _ -> ma 

Таким образом, если первое значение Nothing, функция не не выполняется (так что нет второго побочного эффекта), но если первое значение Just, то функция запускается (с побочным эффектом). Я сохраняю эффект второго вычисления (например, Nothing делает целое выражение Nothing), но исходное значение.

Теперь я задаюсь вопросом. Может ли он работать на любую монаду?

Он выглядит как бы встроенный >>, но я ничего не нашел в стандартной библиотеке.

+2

В общем, вы не можете ничего сделать, чтобы «выбросить» эффект монадического действия. Если вы действительно хотите сохранить оба эффекта, то ваша функция будет '\ x f -> x >> = \ a -> f a >> return a'. – user2407038

+1

В самом общем случае * результат *, заданный монадической функцией, зависит от его * efect * (или его тайны), поэтому я не думаю, что вы можете просто избежать эффекта некоторой избранной монадической функции. (Я думаю об IO ... что, если 'ma' берет' a' со стандартного ввода?) – Euge

ответ

11

Давайте пройдем через это!

keepValue :: Monad m => m a -> (a -> m b) -> m a 
keepValue ma f = _ 

Так что мы хотим keepValue делать? Ну, первое, что мы должны сделать, это использовать ma, поэтому мы можем подключить его к f.

keepValue :: Monad m => m a -> (a -> m b) -> m a 
keepValue ma f = do 
    a <- ma 
    _ 

Теперь мы имеем значение va типа a, поэтому мы можем передать его f.

keepValue :: Monad m => m a -> (a -> m b) -> m a 
keepValue ma f = do 
    va <- ma 
    vb <- f va 
    _ 

И, наконец, мы хотим производить va, так что мы можем просто сделать:

keepValue :: Monad m => m a -> (a -> m b) -> m a 
keepValue ma f = do 
    va <- ma 
    vb <- f va 
    return va 

Это как бы я пройти через написание первого проекта любого монадической функции, как это. Тогда я почистил бы его. Во-первых, некоторые мелочи: с Applicative является суперклассом Monad, я предпочитаю pure по return; мы не использовали vb; и я бы сбросил v в именах. Так на основе версии с do -notation этой функции, я думаю, что лучший вариант

keepValue :: Monad m => m a -> (a -> m b) -> m a 
keepValue ma f = do 
    a <- ma 
    _ <- f a 
    pure a 

Теперь, однако, мы можем начать, чтобы сделать реализацию лучше. Во-первых, мы можем заменить _ <- f va с явным вызовом (>>):

keepValue :: Monad m => m a -> (a -> m b) -> m a 
keepValue ma f = do 
    a <- ma 
    f a >> pure a 

И теперь мы можем применить упрощение. Вы знаете, что мы всегда можем заменить (>>=) плюс pure/return с fmap/(<$>): любой из pure . f =<< ma, ma >>= pure . f или do a <- ma ; pure $ f a (все из которых эквивалентны) могут быть заменены f <$> ma. Тем не менее, Functor класса типа имеет другой, менее известный метод, (<$):

(<$) :: a -> f b -> f a
заменить все места в входе с тем же значением. По умолчанию используется значение fmap . const, но это может быть отменено более эффективной версией.

Таким образом, мы имеем аналогичное правило замены для (<$): мы всегда можем заменить ma >> pure b или do ma ; pure b с b <$ ma. Это дает нам

keepValue :: Monad m => m a -> (a -> m b) -> m a 
keepValue ma f = do 
    a <- ma 
    a <$ f a 

И я думаю, что это самая короткая разумная версия этой функции! Нет никаких хороших точных трюков, чтобы сделать это чище; одним из индикаторов является множественное использование a во второй строке блока do.


Кстати, терминология примечание: вы работаете два монадическое действие или два монадического значение; вы не работаете * «две монады». Монада - это что-то вроде Maybe - конструктор типа, который поддерживает (>>=) и return. Не смешивайте значения с типами - такое терминологическое различие помогает держать вещи яснее!

+2

Вы имеете в виду 'import Control.Arrow; keepValue ma f = ma >> = uncurry (>>). (f &&& return) 'не считается хорошим трюком? ; -P – ephemient

+2

@ephemient: Я предпочитаю '(. ((<$) <*>)). (>> =)', лично :-) Или 'ma >> = (<$) <*> f', если вам действительно нравятся имена переменных! –

+1

Это времена, когда даже кодировщики perl бледнеют от зависти к нам, разыгрывают, потому что, когда наши кошки ходят по клавиатуре, это тип проверяется – epsilonhalbe

1

Эта структура очень похожа на определение >>=/>> для Monad Maybe.

case foo of 
    Nothing -> Nothing 
    Just _ -> bar 

foo >>= \_ -> bar 
foo >> bar 

поэтому ваше исходное выражение может быть упрощено до

ma >>= f >> ma 

и это работает для других монад.

Однако, я не думаю, что это на самом деле то, что вы хотите, так как вы видите ma, происходящий дважды. Вместо этого возьмите значение из первого связывания ma >>= и переносите его до конца вычисления.

keepValue ma f = 
    ma >>= \a -> 
    f a >> 
    return a 

или do -notation

keepValue ma f = do 
    a <- ma 
    f a 
    return a 
+2

Пример того, почему вы не хотите 'ma >> = f >> ma': что-то делает очень отличается в 'IO', когда' ma' - это что-то вроде «потратить деньги»!:-) Или более прозаично: 'getLine >> = print >> getLine' читается в двух строках,' keepValue getLine print' читает только один. –