简体   繁体   English

避免在Haskell中显式递归

[英]Avoiding explicit recursion in Haskell

The following simple function applies a given monadic function iteratively until it hits a Nothing, at which point it returns the last non-Nothing value. 下面的简单函数迭代地应用给定的monadic函数,直到它命中Nothing,此时它返回最后一个非Nothing值。 It does what I need, and I understand how it works. 它做我需要的,我理解它是如何工作的。

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

As part of my self-education in Haskell I'm trying to avoid explicit recursion (or at least understand how to) whenever I can. 作为我在Haskell的自我教育的一部分,我试图尽可能避免明确的递归(或至少理解如何)。 It seems like there should be a simple non-explicitly recursive solution in this case, but I'm having trouble figuring it out. 在这种情况下,似乎应该有一个简单的非显式递归解决方案,但我无法搞清楚。

I don't want something like a monadic version of takeWhile , since it could be expensive to collect all the pre-Nothing values, and I don't care about them anyway. 我不想要类似于takeWhile 版本takeWhile ,因为收集所有pre-Nothing值可能会很昂贵,而且无论如何我都不关心它们。

I checked Hoogle for the signature and nothing shows up. 我检查了Hoogle的签名,没有任何显示。 The m (Maybe a) bit makes me think a monad transformer might be useful here, but I don't really have the intuitions I'd need to come up with the details (yet). m (Maybe a)位让我觉得monad变换器在这里可能很有用,但我真的没有直觉来提出细节(还)。

It's probably either embarrassingly easy to do this or embarrassingly easy to see why it can't or shouldn't be done, but this wouldn't be the first time I've used self-embarrassment as a pedagogical strategy. 这可能要么是令人尴尬地容易做到这一点,要么令人尴尬地容易理解为什么不能或不应该这样做,但这不是我第一次将自我尴尬作为一种教学策略。

Update: I could of course provide a predicate instead of using Maybe : something like (a -> Bool) -> (a -> ma) -> a (returning the last value for which the predicate is true) would work just as well. 更新:我当然可以提供谓词而不是使用Maybe :类似(a -> Bool) -> (a -> ma) -> a (返回谓词为true的最后一个值)也可以正常工作。 What I'm interested in is a way to write either version without explicit recursion, using standard combinators. 我感兴趣的是使用标准组合器编写没有显式递归的任一版本的方法。


Background: Here's a simplified working example for context: suppose we're interested in random walks in the unit square, but we only care about points of exit. 背景:这是一个简化的上下文工作示例:假设我们对单位广场中的随机游走感兴趣,但我们只关心退出点。 We have the following step function: 我们有以下步骤功能:

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')

Something like evalState (lastJustM (randomStep 0.01) (0.5, 0.5)) <$> newStdGen will give us a new data point. evalState (lastJustM (randomStep 0.01) (0.5, 0.5)) <$> newStdGen会给我们一个新的数据点。

A lot of what avoiding explicit recursion is about is composing built-in recursive combinators, which usually work on very generic unlifted values. 避免显式递归的很多内容是组成内置的递归组合器,它通常用于非常通用的未提升值。 Doing the same thing in a Functor, Monad, or other lifted type sometimes works using basic lifting operations like fmap , <*> , >>= , and so on. 在Functor,Monad或其他提升类型中执行相同操作有时可以使用基本提升操作,如fmap<*>>>=等。 In some cases a pre-lifted version is already present, as with mapM , zipWithM , and so on. 在某些情况下,已经存在预先提升的版本,如mapMzipWithM等。 Other times, as with takeWhile , lifting is not trivial and no built-in version is provided. 其他时候,与takeWhile ,提升并takeWhile ,也没有提供内置版本。

Your function indeed has characteristics of something that should be a lifted version of standard combinators. 你的功能确实具有应该成为标准组合器的升级版本的特性。 So first, let's strip out the monad to reconstruct the function you're implicitly lifting: 首先,让我们去掉monad来重建你隐含提升的函数:

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

The word "last" here gives us a hint; 这里的“最后”这个词给了我们一个提示; non-explicit recursion often uses temporary lists as control structures. 非显式递归通常使用临时列表作为控制结构。 So what you want is last applied to the list generated by iterating the function until getting Nothing . 所以你想要的是last应用到迭代函数生成的列表,直到得到Nothing With a slight generalization of the type, we find the generator: 通过类型的略微概括,我们找到了生成器:

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

So we have something like this: 所以我们有这样的事情:

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

Unfortunately at this point we're kind of stuck, because (to my knowledge) there's no monadic unfold and lifting it is, like takeWhile , not trivial. 不幸的是,在这一点上,我们有点卡住了,因为(据我所知)没有takeWhile展开并解除它,就像takeWhile一样,并非琐碎。 Another thing that could make sense is a more general unfold with a signature like (MonadMaybe m) => (b -> m (a, b)) -> b -> m [a] and an accompanying MaybeT transformer, but that doesn't exist in the standard libraries either, and monad transformers are kind of a Pit of Despair anyway. 另一件可能有意义的事情是更一般的展开,签名如(MonadMaybe m) => (b -> m (a, b)) -> b -> m [a]和伴随的MaybeT变换器,但这不是也不存在于标准库中,monad变换器无论如何都是绝望之坑。 A third approach might be to find some way to generalize both Maybe and the unknown monad out as MonadPlus or something similar. 第三种方法可能是找到一些方法将Maybe和未知monad概括为MonadPlus或类似的东西。

Of course, there may be other approaches with a different structure, but I suspect you're likely to find similar awkwardness with any function that needs to be recursive going "into" a monad, eg, each step conceptually introduces another layer that must be eliminated with >>= , join , etc. 当然,可能有其他方法具有不同的结构,但我怀疑你可能会发现任何需要递归进入“monad”的函数的类似尴尬,例如,每一步在概念上引入了另一层必须是用>>=join等消除

In summary: At first inspection your function as written is best expressed without explicit recursion, using a recursive combinator (some flavor of unfoldM ) that doesn't exist. 总结:首先检查你写的函数最好是在没有显式递归的情况下表达,使用一个不存在的递归组合器(某些展开的unfoldM )。 You can either write the combinator yourself (as people have done with takeWhileM ), go digging around on Hackage for monadic recursive combinators, or just leave your code as is. 您可以自己编写组合子(就像人们使用takeWhileM ),在Hackage上搜索monadic递归组合器,或者只是保留代码。

I don't want something like a monadic version of takeWhile , since it could be expensive to collect all the pre-Nothing values, and I don't care about them anyway. 我不想要类似于takeWhile版本的takeWhile ,因为收集所有pre-Nothing值可能会很昂贵,而且无论如何我都不关心它们。

Monadic-lists takeWhile does not collect all the pre-Nothing values unless you explicitly want to do that. takeWhile -lists takeWhile不会收集所有pre-Nothing值,除非您明确要这样做。 This would be the takeWhile from the "List" package , used in this answer to the very question you linked to. 这将是来自“List”包takeWhile ,用于回答您链接的问题。

As for the function that you wish to implement: 至于你想要实现的功能:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM