避免Haskell中的显式递归

避免Haskell中的显式递归,haskell,functional-programming,monads,Haskell,Functional Programming,Monads,下面的简单函数迭代地应用给定的一元函数,直到它遇到一个Nothing,在该点上返回最后一个non-Nothing值。它能满足我的需要,我知道它是如何工作的 lastJustM :: (Monad m) => (a -> m (Maybe a)) -> a -> m a lastJustM g x = g x >>= maybe (return x) (lastJustM g) 作为我在Haskell自学的一部分,我尽量避免显式递归(或者至少理解如何避免)。在

下面的简单函数迭代地应用给定的一元函数,直到它遇到一个Nothing,在该点上返回最后一个non-Nothing值。它能满足我的需要,我知道它是如何工作的

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(可能是a)
位让我觉得monad转换器在这里可能有用,但我真的没有直觉,我需要想出细节(还没有)

做这件事可能很容易,也可能很容易理解为什么不能做或不应该做,但这不是我第一次将自我尴尬作为一种教学策略

更新:我当然可以提供一个谓词,而不是使用
也许
:类似
(a->Bool)->(a->ma)->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')
randomStep::(浮动a、Ord a、随机a)=>
a->(a,a)->州标准(可能(a,a))
随机步骤s(x,y)=do

(a,gen')很多避免显式递归的方法都是构造内置的递归组合器,它通常处理非常通用的未刷新值。在函子、单子或其他提升类型中执行相同的操作有时可以使用基本的提升操作,如
fmap
>=
,等等。在某些情况下,已经存在预提升版本,如
mapM
zipWithM
,等等。其他情况下,与
takeWhile
一样,提升并不简单,也不提供内置版本

您的函数确实具有某些特性,这些特性应该是标准组合符的升级版本。首先,让我们去掉monad来重建隐式提升的函数:

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
转换器,但这在标准库中也不存在,而且monad transformer是一个绝望的坑。第三种方法可能是找到某种方法,将
Maybe
和未知的monad概括为
MonadPlus
或类似的东西

当然,可能有其他不同结构的方法,但我怀疑您可能会发现,任何需要“进入”单子的递归函数都有类似的尴尬,例如,每个步骤在概念上都引入了另一层,必须使用
>=
连接
,等等来消除

总而言之:在第一次检查时,您编写的函数最好不用显式递归,而是使用一个不存在的递归组合器(某种类型的
unfolm
)。您可以自己编写组合器(就像人们使用
takeWhileM
所做的那样),深入研究一元递归组合器的黑客攻击,或者干脆让代码保持原样

我不想要像takeWhile的一元版本,因为收集所有的pre-Nothing值可能会很昂贵,而且我不在乎它们

一元列表
takeWhile
不会收集所有无前置值,除非您明确希望这样做。这将是中的
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

lastJustM=fix(liftm2ap((>>=)。(翻转(可能.返回)。)
。(好吧,我用
pointfree
作弊了)@KennyTM:谢谢!我甚至没有想过尝试
pointfree
,因为我不知道它能够处理这种事情。现在我只需要弄清楚它是如何工作的,有一种算法,只要给几个组合子,就可以把任何东西简化成无点形式;这就是无点
所使用的。当然,结果可能有用,也可能不有用:)不要被名称搞混了,
pointfree
输出的正确术语是“无意义样式”。那
liftM2
是否在
((->)r)
单子中工作?这总是能提高代码的清晰度。
fix
的真正问题在于,除了“如果我弄糟了,程序会崩溃”之外,它没有任何语义价值。像
map
fold
这样的东西的价值在于,只要你不留下一个无限列表,它们就完美地定义了它们执行的操作,不会让你误入歧途。答案不错,但我发现递归版本非常清晰。你真的认为使用某种一元展开组合器会改进它吗