2010-05-18 6 views
10

Следующая простая функция применяет данную монадическую функцию итеративно, пока она не ударит ничто, и в этот момент она возвращает последнее значение, отличное от Ничего. Он делает то, что мне нужно, и я понимаю, как это работает.Избегание явной рекурсии в Haskell

lastJustM :: (Monad m) => (a -> m (Maybe a)) -> a -> m a 
lastJustM g x = g x >>= maybe (return x) (lastJustM g) 

В рамках моего самообразования в Haskell Я пытаюсь избежать явной рекурсии (или, по крайней мере, понять, как это сделать) всякий раз, когда я могу. Похоже, в этом случае должно быть простое неявное рекурсивное решение, но мне сложно понять это.

Я не хочу, чтобы что-то вроде a monadic version из takeWhile, так как это может быть дорого собрать все значения до-Ничего, и я все равно их не волнует.

Я проверил Hoogle на подпись, и ничего не появляется. Сбой m (Maybe a) заставляет меня думать, что здесь может быть полезен трансформатор монады, но у меня нет действительно интуиций, которые мне нужны, чтобы придумать детали (пока).

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

Update: Я мог бы, конечно, обеспечить предикат вместо использования Maybe: что-то вроде (a -> Bool) -> (a -> m a) -> a (возвращая последнее значение, для которых предикат является истинным) будет работать так же хорошо. Меня интересует способ записи любой версии без явной рекурсии с использованием стандартных комбинаторов.


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

randomStep :: (Floating a, Ord a, Random a) => 
       a -> (a, a) -> State StdGen (Maybe (a, a)) 
randomStep s (x, y) = do 
    (a, gen') <- randomR (0, 2 * pi) <$> get 
    put gen' 
    let (x', y') = (x + s * cos a, y + s * sin a) 
    if x' < 0 || x' > 1 || y' < 0 || y' > 1 
    then return Nothing 
    else return $ Just (x', y') 

Что-то вроде evalState (lastJustM (randomStep 0.01) (0.5, 0.5)) <$> newStdGen даст нам новую точку данных.

+2

'lastJustM = FIX (.. LiftM2 ап ((>> =)) (флип (возможно, вернется))..)'. (ОК, я обманул с помощью 'pointfree'.) – kennytm

+1

@KennyTM: Спасибо! Я даже не думал попробовать «бесполезно», потому что я понятия не имел, что он сможет справиться с такими вещами. Теперь мне просто нужно выяснить, как это работает. –

+1

Существует алгоритм для сокращения * ничего * до беспутной формы с учетом комбинации комбинаторов; это то, что использует 'pointfree'. Конечно, результат может быть или не быть полезным :) –

ответ

9

Многое из того, что позволяет избежать явной рекурсии, заключается в создании встроенных рекурсивных комбинаторов, которые обычно работают с очень общими неактивными значениями. Выполнение того же самого в Functor, Monad или другом поднятом типе иногда работает с использованием основных операций по подъему, таких как fmap, <*>, >>= и так далее. В некоторых случаях уже имеется предварительно поднятая версия, например mapM, zipWithM и т. Д. В других случаях, как и в случае с takeWhile, подъем не является тривиальным и не предусмотрена встроенная версия.

Ваша функция действительно имеет характеристики того, что должно быть поднятой версией стандартных комбинаторов. Поэтому первое, давайте раздеть монаду, чтобы восстановить функцию вы неявно подъем:

lastJust :: (a -> Maybe a) -> a -> a 

Слово «последний» здесь дает нам подсказку; неявная рекурсия часто использует временные списки в качестве структур управления. Итак, вы хотите, чтобы last применялся к списку, сгенерированному путем итерации функции до получения Nothing.С небольшим обобщением типа, мы находим генератор:

unfoldr :: (b -> Maybe (a, b)) -> b -> [a] 

Таким образом, мы имеем что-то вроде этого:

dup x = (x, x) 
lastJust f x = last $ unfoldr (fmap dup . f) x 

К сожалению, на данный момент мы вроде застрял, потому что (к моему знанию) нет монадического разворота и подъема, как, например, takeWhile, а не тривиально. Еще одна вещь, которая может иметь смысл, - это более общий разворот с сигнатурой вроде (MonadMaybe m) => (b -> m (a, b)) -> b -> m [a] и сопроводительным трансформатором MaybeT, но это тоже не существует в стандартных библиотеках, а монадные трансформаторы - это, как ни странно, Пит Отчаяния. Третий подход может заключаться в том, чтобы найти способ обобщить как Maybe так и неизвестную монаду как MonadPlus или что-то подобное.

Конечно, могут быть другие подходы с другой структурой, но я подозреваю, что вы, вероятно, найдете такую ​​же неловкость с любой функцией, которая должна быть рекурсивной, «войдет» в монаду, например, каждый шаг концептуально вводит другой слой, который должен быть устранен с >>=, join и т.д.

В заключении: при первой проверке вашей функции, как написано лучше всего выражаются без явной рекурсии, используя рекурсивный комбинатор (некоторый привкус unfoldM), который не существует. Вы можете написать комбинатор самостоятельно (как это делали люди с takeWhileM), идите копаться в Hackage для монадических рекурсивных комбинаторов или просто оставляйте свой код как есть.

+0

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

+1

@ Норман Рамси: Может быть (не каламбур), я полагаю. Мне нравятся стандартные комбинаторы, когда они делают семантическую структуру более очевидной, в этом случае она неявно генерирует и сворачивает список результатов (гиломорфизм, если я правильно помню терминологию?). По той же причине мне нравится кодировать в монадах и т. Д., Поднимая лежащую в основе логику, например, '<$>' и' <*>' вместо сложных, императивных блоков 'do'. В этом случае функция достаточно проста, как есть, и необходимого комбинатора не существует, поэтому я, вероятно, не стал бы беспокоиться сам. –

+0

@Norman Ramsey: С другой стороны, это может быть просто признаком того, что мне нужно убрать учебники по теории категорий и пытаться перестать замечать корундирование повсюду ... –

3

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

Monadic-lists takeWhile не собирает все значения pre-Nothing, если вы явно не хотите этого делать. Это будет takeWhile от "List" package, использовано в this answer по самому вопросу, с которым вы столкнулись.

Что касается функции, которую вы хотите реализовать:

{-# LANGUAGE ScopedTypeVariables #-} 

import Control.Monad.ListT (ListT) -- from "List" package on hackage 
import Data.List.Class (takeWhile, iterateM, lastL) 
import Prelude hiding (takeWhile) 

thingM :: forall a m. Monad m => (a -> Bool) -> (a -> m a) -> m a -> m a 
thingM pred stepM startM = 
    lastL $ takeWhile pred list 
    where 
     list :: ListT m a 
     list = iterateM stepM startM 
+0

Мне интересно узнать больше о монадических списках - я до сих пор не понимаю, как они избегают сбора значений, например. У меня еще не было возможности работать с страницей «ListT done right» в wiki, но есть ли у вас какие-либо предложения по другим отправным точкам, которые вряд ли вы найдете в Googling «listt haskell», «monadic» списки "и т. д.? –

+1

@Travis Brown: Я не уверен, что вы подразумеваете под «собиранием ценностей» здесь. Если элементы списка отбрасываются так быстро, как они созданы, полный список никогда не будет создан сразу. Это то, что я имел в виду выше, о списках, которые являются «структурой управления» - свертывание списка в основном рекурсивно, когда стек вызовов указан «раньше времени», часто лениво. –

+0

@camccann: Думаю, я просто смущаюсь о том, как монада и лень взаимодействуют здесь. Я заткнись, пока у меня не будет возможности читать и думать. Спасибо за ваши ответы. –