【问题标题】:Avoiding explicit recursion in Haskell在 Haskell 中避免显式递归
【发布时间】:2010-05-18 16:47:35
【问题描述】:

下面的简单函数迭代地应用给定的一元函数,直到它遇到一个 Nothing,此时它返回最后一个非 Nothing 值。它可以满足我的需要,而且我了解它的工作原理。

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

作为我在 Haskell 中自我教育的一部分,我试图尽可能避免显式递归(或者至少了解如何避免)。在这种情况下,似乎应该有一个简单的非显式递归解决方案,但我无法弄清楚。

我不想要 takeWhile 中的 a monadic version 之类的东西,因为收集所有 pre-Nothing 值可能很昂贵,而且我也不关心它们。

我检查了 Hoogle 的签名,但没有任何显示。 m (Maybe a) 位让我认为 monad 转换器在这里可能有用,但我真的没有直觉我需要想出细节(还)。

这样做可能很容易令人尴尬,或者很容易看出为什么不能或不应该这样做,但这不是我第一次将自我尴尬作为一种教学策略。

更新:我当然可以提供一个谓词而不是使用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)) &lt;$&gt; newStdGen 这样的东西会给我们一个新的数据点。

【问题讨论】:

  • lastJustM = fix (liftM2 ap ((&gt;&gt;=) .) . (flip (maybe . return) .))。 (好吧,我欺骗了pointfree。)
  • @KennyTM:谢谢!我什至没有想过要尝试pointfree,因为我不知道它能够处理这种事情。现在我只需要弄清楚它是如何工作的。
  • 有一种算法可以将 anything 简化为给定少量组合子的无点形式;这就是pointfree 使用的。当然,结果可能有用也可能没用:)
  • 不要被名字所迷惑,pointfree 的输出的正确术语是“无意义的风格”。那是liftM2((-&gt;) r) monad 中工作吗?这总能提高代码的清晰度...
  • fix 的真正问题在于它除了“如果我搞砸了程序就会崩溃”之外没有任何语义价值。 mapfold 之类的东西的价值在于,只要你不留下折叠无限列表,它们就完美地定义了它们执行的操作,不会让你误入歧途。

标签: haskell functional-programming monads


【解决方案1】:

避免显式递归的大部分内容是编写内置递归组合器,这些组合器通常适用于非常通用的未提升值。在 Functor、Monad 或其他提升类型中执行相同的操作有时可以使用基本提升操作,例如 fmap&lt;*&gt;&gt;&gt;= 等。在某些情况下,已经存在预提升版本,如 mapMzipWithM 等。其他时候,和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) =&gt; (b -&gt; m (a, b)) -&gt; b -&gt; m [a] 这样的签名和一个伴随的MaybeT 转换器,但这在标准库中也不存在,而且单子转换器无论如何都是绝望的深渊。第三种方法可能是找到某种方法将Maybe 和unknown monad 概括为MonadPlus 或类似的东西。

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

总而言之:首先检查您所写的函数最好在没有显式递归的情况下表达,使用不存在的递归组合器(unfoldM 的某种风格)。您可以自己编写组合器(就像人们对 takeWhileM 所做的那样),在 Hackage 上挖掘一元递归组合器,或者保持您的代码不变。

【讨论】:

  • 不错的答案,但我发现递归版本非常明显。你真的认为它会通过使用某种单子展开组合器来改进吗?
  • @Norman Ramsey:我想可能(不是双关语)。我喜欢标准组合器,因为它们使语义结构更加明显,在这种情况下,它隐式生成并折叠结果列表(如果我没记错术语,是一种hylomorphism?)。出于同样的原因,我喜欢通过提升底层逻辑(例如 &lt;$&gt;&lt;*&gt; 而不是复杂的、命令式的 do 块)来编写 monad 等代码。在这种情况下,该函数很简单,并且不存在必要的组合器,所以我自己可能不会打扰。
  • @Norman Ramsey:另一方面,这可能只是表明我需要放下范畴论教科书并尽量停止到处注意核心递归......
  • @Norman Ramsey:在 camccann 的辩护中,我并没有真正要求改进或替代我代码中的递归版本的东西——这更像是一种心理锻炼,我发现这个答案非常在这方面很有帮助。
【解决方案2】:

我不想要像 takeWhile 这样的单子版本,因为收集所有 pre-Nothing 值可能很昂贵,而且我也不关心它们。

Monadic-lists takeWhile 不会收集所有 pre-Nothing 值,除非您明确想要这样做。这将是 "List" package 中的 takeWhile,在 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

【讨论】:

  • 我有兴趣了解更多关于单子列表的信息——例如,我仍然不明白它们是如何避免收集值的。我还没有机会浏览 wiki 上的“ListT done right”页面,但是你对我在谷歌搜索“listt haskell”、“monadic”时可能找不到的其他起点有什么建议吗列表”等?
  • @Travis Brown:我不确定您所说的“收集值”是什么意思。如果列表元素被丢弃的速度与创建它们的速度一样快,那么整个列表实际上永远不会一次构建。这就是我上面提到的列表是“控制结构”的意思——折叠列表基本上是递归的,调用堆栈“提前”指定,通常是懒惰的。
  • @camccann:我想我只是对 monad 和 laziness 在这里交互的方式感到困惑。我会闭嘴直到我有机会做更多的阅读和思考。感谢您的回答。
  • @Travis Brown:懒惰是懒惰的,直到它不是,不幸的是。单子通常无关紧要,但隐藏在 &gt;&gt;= 操作中的机制可以增加不同程度的严格性。 Identity 强制绑定链,State 对 state 参数的使用施加额外的严格性,IO 对接触外部世界的事物强制严格,等等。所有这些单子列表组合器的用途是不强制超过必要的;否则,天真的 takeWhileM f xs = sequence xs &gt;&gt;= (return . takeWhile f) 会起作用。
  • @Travis Brown:对于普通列表,takeWhile 并不一定意味着列表的 takeWhiled 部分存储在内存中——这完全取决于之后对结果列表的处理。同样的情况。 monadic takeWhile 不会在“调用它时”使用列表,而是返回另一个列表。根据您以后对它的处理,它可能存储在内存中,也可能不存储。在这种情况下,它不会存储在内存中,因为lastL 使用它而不保存它的值。
猜你喜欢
  • 2020-02-18
  • 1970-01-01
  • 2022-06-14
  • 1970-01-01
  • 2018-12-28
  • 2015-04-29
  • 1970-01-01
  • 2017-01-31
  • 1970-01-01
相关资源
最近更新 更多