Избегание явной рекурсии в Haskell

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

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

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

Мне не нужно что-то вроде монадической версии takeWhile, так как было бы дорого собрать все значения pre-Nothing, В любом случае, заботиться о них.

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

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

Обновление: Я мог бы, конечно, предоставить предикат вместо использования 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 даст нам новую точку данных.

Ответ 1

Многое из того, что избегает явной рекурсии, заключается в составлении встроенных рекурсивных комбинаторов, которые обычно работают на очень общих неактивных значениях. Выполнение того же самого в 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, но этого также нет в стандартных библиотеках, а монадные трансформаторы - это, как ни странно, Pit of Despair. Третий подход может заключаться в том, чтобы найти способ обобщить как Maybe так и неизвестную монаду как MonadPlus или что-то подобное.

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

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

Ответ 2

Мне не нужно что-то вроде монадической версии takeWhile, так как было бы дорого собрать все значения pre-Nothing, и я все равно их не забочусь.

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

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

{-# 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