【问题标题】:Loop through a set of functions with Haskell使用 Haskell 循环遍历一组函数
【发布时间】:2012-03-06 04:38:48
【问题描述】:

这是一个简单的准系统示例,说明我尝试编写的代码在 C++ 中的外观。

while (state == true) {
  a = function1();
  b = function2();
  state = function3();
}

在我正在处理的程序中,我需要循环一些函数,直到 bool state 等于 false(或者直到其中一个变量,比如说变量 b,等于 0) .

如何在 Haskell 中完成这段代码?我在这里、谷歌甚至 Bing 都进行了搜索,但无法找到任何关于如何使用函数执行重复操作的清晰、直接的解释。

任何帮助将不胜感激。

【问题讨论】:

  • 您发布的代码本质上是必要的。虽然可以在 Haskell 中以命令式风格编写代码,但这通常不是最好的方法。我认为您应该从代码正在做什么的更高级别的概念开始,并尝试将其转化为功能解决方案。
  • 你是对的......我仍在努力将我的 C++ 思维方式转换为函数式语言。但是如果没有某种循环,我想不出任何其他方法来做到这一点。在我的程序中,必须多次调用某些函数,直到满足所有条件。
  • 您应该告诉我们您在此处实际上要做什么,以便我们可以帮助您将其改造成功能性解决方案。
  • 我正在尝试制作一个通过自然语言处理运行的数据分析应用程序。 >_

标签: haskell loops recursion while-loop list-comprehension


【解决方案1】:

考虑到 Daniels 的评论,它可能看起来像这样:

f = loop init_a init_b true
    where
         loop a b True = loop a' b' (fun3 a' b')
             where
                 a' = fun1 ....
                 b' = fun2 .....
         loop a b False = (a,b)

【讨论】:

  • 所以 Haskell 确实有一个循环??如果该功能存在,我不妨使用它。 :D 但我会尝试为我的下一个项目提前计划好,并以使用递归而不是循环的方式构建程序。我确实想学习如何以正确的方式使用 Haskell。谢谢。
  • @SubtleArray 我认为你误解了一些东西,上面的代码定义了一个名为循环的函数。它没有使用任何原始循环结构(也许递归除外)。
  • 我刚刚学会了这一点。我当时想“为什么这个循环不起作用??” :D 虽然效果很好。现在,我正在尝试使用调用其他函数的模式匹配函数将其塑造成更 Haskelly 和递归的东西。我也许可以让它工作。
  • 也就是说,Haskell 中实际上有 “循环”结构,最值得注意的是 untiliterate
  • +1 并为直截了当的答案表示敬意。 fun1fun2 也可以引用状态,所以可能应该在那里创建第三个变量:loop a b c@True = loop a' b' c' where ... ; c' = fun3 ... 此外,严格性注释(爆炸模式)可能是循环规范不可分割的一部分,因为通常需要恒定空间操作。
【解决方案2】:

您应该以更实用的方式编写问题的解决方案。 然而,haskell 中的一些代码很像命令式循环,例如状态单子、终端递归、untilfoldr 等。

一个简单的例子是阶乘。在 C 语言中,您可以编写一个循环,例如在 haskell 中您可以编写 fact n = foldr (*) 1 [2..n]

【讨论】:

  • 感谢您的回复。有没有办法将 untilfoldr 应用于函数而不是整数?
  • 这里 foldr 应用于函数 multiply (*)。第一个状态由1 表示,然后通过结合前一个状态和列表中的参数来修改“当前循环结果”。
  • unfoldr 是另一个有用的“循环”操作。
【解决方案3】:

如果您有两个函数 f :: a -> bg :: b -> c,其中 abcString[Int] 之类的类型,那么您只需编写 f . b 即可组成它们.

如果你希望它们遍历一个列表或向量,你可以写map (f . g)V.map (f . g),假设你已经完成了Import qualified Data.Vector as V

示例:我希望打印诸如 ## <number>. <heading> ## 之类的降价标题列表,但我需要从 1 开始编号的罗马数字,而我的列表 headings 的类型为 [(String,Double)],其中 Double 无关紧要。

Import Data.List
Import Text.Numeral.Roman
let fun = zipWith (\a b -> a ++ ". " ++ b ++ "##\n") (map toRoman [1..]) . map fst
fun [("Foo",3.5),("Bar",7.1)]

这到底是做什么的?

toRoman 将数字转换为包含罗马数字的字符串。 map toRoman 对循环的每个元素执行此操作。 map toRoman [1..] 对惰性无限列表 [1,2,3,4,..] 的每个元素执行此操作,产生一个罗马数字字符串的惰性无限列表

fst :: (a,b) -> a 只是提取元组的第一个元素。 map fst 在整个列表中丢弃了我们愚蠢的 Meow 信息。

\a b -> "##" ++ show a ++ ". " ++ b ++ "##" 是一个 lambda 表达式,它接受两个字符串并将它们连接到所需的格式字符串中。

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] 接受一个像我们的 lambda 表达式一样的双参数函数,并从它自己的第二个和第三个参数中为其提供一对元素。

您会发现zipzipWith 等仅读取标题列表所需的罗马数字的惰性无限列表,这意味着我在不维护任何计数器变量的情况下为标题编号.

最后,我声明了fun,但没有命名它的参数,因为编译器可以从map fst 需要一个参数这一事实中找出答案。你会注意到在我的第二张地图之前也放了一个.。如果我写了fun h = ...,我可以写(map fst h)$ map fst h,但是离开fun 的参数意味着我需要在将zipWith 应用于zipWith 想要的三个参数。

我希望编译器通过内联将zipWithmaps 组合成一个循环。

【讨论】:

    【解决方案4】:

    好吧,这里有一个关于如何在此处映射概念的建议:

    • C++ 循环是 Haskell 中某种形式的列表操作。
    • 循环的一次迭代 = 处理列表中的一个元素。
    • 循环直到某个条件变为真 = 在列表中递归的函数的基本情况。

    但是在命令式循环和函数式列表函数之间有一些非常不同的地方:循环描述如何进行迭代;高阶列表函数描述了计算的结构。比如map f [a0, a1, ..., an]可以用这张图来描述:

    [a0,   a1,   ..., an]
     |     |          |
     f     f          f
     |     |          |
     v     v          v
    [f a0, f a1, ..., f an]
    

    请注意,这里描述的是结果如何与参数f[a0, a1, ..., an] 相关,而不是如何逐步执行迭代。

    同样,foldr f z [a0, a1, ..., an] 对应于:

    f a0 (f a1 (... (f an z)))
    

    filter 不太适合绘制图表,但很容易说明它满足的许多规则:

    • length (filter pred xs) <= length xs
    • 对于filter pred xs 中的每个元素xpred xTrue
    • 如果xfilter pred xs 的一个元素,那么xxs 的一个元素
    • 如果x 不是xs 的元素,则x 不是filter pred xs 的元素
    • 如果xfilter pred xs 中出现在x' 之前,那么xxs 中出现在x' 之前
    • 如果x出现在xs中的x'之前,并且xx'都出现在filter pred xs中,那么x出现在filter pred xs中的x'之前

    在经典的命令式程序中,所有这三种情况都写成循环,它们之间的区别归结为循环体的作用。相反,函数式编程坚持这种结构模式不属于“循环体”(这些示例中的函数fpred);相反,最好将这些模式抽象为高阶函数,如mapfoldrfilter。因此,每次您看到这些列表函数之一时,您都会立即了解有关参数和结果如何相关的一些重要事实,而无需阅读任何代码;而在典型的命令式程序中,您必须阅读循环体才能弄清楚这些内容。

    因此,您的问题的真正答案是,在不知道循环体在做什么的情况下,不可能将命令式循环惯用地翻译成函数式术语——循环运行之前的前提条件是什么,以及后置条件应该是循环结束的时候。因为你只是模糊描述的那个循环体将决定计算的结构是什么,而不同的这种结构将在 Haskell 中调用不同的高阶函数。

    【讨论】:

      【解决方案5】:

      首先,让我们考虑一些事情。

      • function1 有副作用吗?
      • function2有副作用吗?
      • function3 有副作用吗?

      所有这些问题的答案都是非常明显的“是”,因为它们不接受任何输入,并且可能在某些情况下会导致您不止一次地绕过 while 循环(而不是 def function3(): return false)。现在让我们用显式状态重塑这些函数。

      s = initialState
      sentinel = true
      while(sentinel):
        a,b,s,sentinel = function1(a,b,s,sentinel)
        a,b,s,sentinel = function2(a,b,s,sentinel)
        a,b,s,sentinel = function3(a,b,s,sentinel)
      return a,b,s
      

      嗯,这很丑陋。我们对每个函数的输入一无所知,也不知道这些函数如何影响变量absentinel,也不知道我简单建模为的“任何其他状态” s.

      所以让我们做一些假设。首先,我将假设这些函数不直接依赖或以任何方式影响absentinel 的值。然而,他们可能会改变“其他状态”。所以这就是我们得到的:

      s = initState
      sentinel = true
      while (sentinel):
        a,s2 = function1(s)
        b,s3 = function2(s2)
        sentinel,s4 = function(s3)
        s = s4
      return a,b,s
      

      请注意,我使用了临时变量 s2s3s4 来指示“其他状态”所经历的变化。哈斯克尔时间。我们需要一个控制函数来表现得像 while 循环。

      myWhile :: s                   -- an initial state
              -> (s -> (Bool, a, s)) -- given a state, produces a sentinel, a current result, and the next state
              -> (a, s)              -- the result, plus resultant state
      myWhile s f = case f s of
        (False, a, s') -> (a, s')
        (True,  _, s') -> myWhile s' f
      

      现在如何使用这样的功能?好吧,鉴于我们有以下功能:

      function1 :: MyState -> (AType, MyState)
      function2 :: MyState -> (BType, MyState)
      function3 :: MyState -> (Bool,  MyState)
      

      我们将构建所​​需的代码如下:

      thatCodeBlockWeAreTryingToSimulate :: MyState -> ((AType, BType), MyState)
      thatCodeBlockWeAreTryingToSimulate initState = myWhile initState f
        where f :: MyState -> (Bool, (AType, BType), MyState)
              f s = let (a, s2) = function1 s
                        (b, s3) = function2 s2
                        (sentinel, s4) = function3 s3
                    in (sentinel, (a, b), s4)
      

      请注意这与上面给出的非丑陋的类似 python 的代码有多么相似。

      您可以通过在三个函数中添加function1 = undefined 等以及在文件顶部添加以下内容来验证我提供的代码是否正确:

      {-# LANGUAGE EmptyDataDecls #-}    
      data MyState
      data AType
      data BType
      

      所以外卖信息是这样的:在 Haskell 中,您必须显式地对状态变化进行建模。您可以使用“State Monad”让事情变得更漂亮,但您应该首先了解传递状态的概念。

      【讨论】:

      • 读者练习:不要使用myWhile,而是尝试使用unfoldr :: (b -> Maybe (a, b)) -> b -> [a] 编写thatCodeBlockWeAreTryingToSimulate。提示:b 类型对应s 类型,Maybe 代替Bool。通过使用unfoldr,您可以获得哪些额外信息?你会丢失什么信息?
      【解决方案6】:

      让我们看看你的 C++ 循环:

      while (state == true) {
        a = function1();
        b = function2();
        state = function3();
      }
      

      Haskell 是一种纯粹的函数式语言,因此如果我们尝试在没有副作用的情况下这样做,它不会与我们产生太大的冲突(并且生成的代码将更加有用,无论是本身还是作为学习 Haskell 的练习),并且没有使用 monads 让它看起来像我们在使用副作用。

      让我们从这个结构开始

      while (state == true) {
         <<do stuff that updates state>>
      }
      

      在 Haskell 中,我们显然不会根据 true 作为循环条件来检查变量,因为它不能改变它的值 [1],我们要么永远评估循环体,要么永远不评估循环体。因此,我们将要评估一个在某个参数上返回布尔值的函数:

      while (check something == True) {
        <<do stuff that updates state>>
      }
      

      好吧,现在我们没有state 变量,所以“做一些更新状态的事情”看起来毫无意义。而且我们没有something 可以传递给check。让我们多考虑一下。我们希望检查something 以依赖于“do stuff”位正在做什么。我们没有副作用,这意味着something 必须(或派生自)从“做的东西”返回。 “做东西”也需要将不同的东西作为参数,否则它会永远返回相同的东西,这也是没有意义的。我们还需要从所有这些中返回一个值,否则我们只是在消耗 CPU 周期(同样,没有副作用,如果我们不以某种方式使用它的输出,那么运行一个函数是没有意义的,而且运行的意义就更少了如果我们从不使用它的输出,就会重复一个函数)。

      那么这样的事情怎么样:

      while check func state =
          let next_state = func state in
          if check next_state
            then while check func next_state
            else next_state
      

      让我们在 GHCi 中尝试一下:

      *Main> while (<20) (+1) 0
      20
      

      这是重复应用(+1)的结果,结果小于20,从0开始。

      *Main> while ((<20) . length) (++ "zob") ""
      "zobzobzobzobzobzobzob"
      

      这是从空字符串开始,在长度小于20的情况下,反复拼接“zob”的结果。

      所以你可以看到我已经定义了一个(有点)类似于命令式语言的while 循环的函数。我们甚至不需要专门的循环语法! (这是 Haskell 没有这种语法的真正原因;如果你需要这种东西,你可以将它表达为一个函数)。这不是唯一的方法,有经验的 Haskell 程序员可能会使用其他标准库函数来完成这种工作,而不是编写 while

      但我认为看看如何在 Haskell 中表达这种东西很有用。它确实表明你不能直接将命令式循环之类的东西翻译成Haskell;我最终没有根据我的while 翻译你的循环,因为它最终毫无意义;你永远不会使用 function1function2 的结果,它们被调用时不带参数,所以它们在每次迭代中总是返回相同的东西,function3 同样总是返回相同的东西,并且只能返回 @ 987654339@ 或 false 会导致 while 继续循环或停止,但不会产生任何信息。

      大概在 C++ 程序中,他们都在使用副作用来实际完成一些工作。如果它们对内存中的事物进行操作,那么您需要一次将程序的较大部分翻译到 Haskell 以使该循环的翻译有意义。如果这些函数正在执行 IO,那么您需要在 Haskell 的 IO monad 中执行此操作,我的 while 函数对此不起作用,但您可以执行类似的操作。


      [1] 顺便说一句,值得尝试理解的是,Haskell 中的“你不能更改变量”不仅仅是一个任意限制,也不仅仅是为了获得纯度的好处而进行的可接受的权衡,它是一个与 Haskell 希望您思考 Haskell 代码的方式不同的概念。您正在写下由对某些参数评估函数产生的表达式:在f x = x + 1 中,您说的是f x x + 1。如果你真的这么想,而不是想“f 取 x,然后加一,然后返回结果”,那么“有副作用”的概念甚至不适用;存在且等于其他事物的事物如何以某种方式改变变量或产生其他副作用?

      【讨论】:

        猜你喜欢
        • 2018-04-09
        • 2021-04-26
        • 2012-10-12
        • 2019-08-13
        • 2013-11-23
        • 1970-01-01
        • 2014-06-04
        相关资源
        最近更新 更多