【问题标题】:Translating a simple imperative algorithm to functional style将简单的命令式算法转换为函数式风格
【发布时间】:2013-10-11 11:37:00
【问题描述】:

我最近做了一个小算法,可以从 sn-p 代码中去除函数参数,只保留最外层的函数。
我发现这个算法很容易以命令式的方式设计。
不过,我对函数式编程真的很感兴趣,我想知道您将如何以函数式的方式完成同样的事情。

如果你能告诉我这样的算法是如何工作的,那将对我非常有帮助,这样我可能会更好地了解函数式编程的工作原理。另外我想知道您在设计算法时的思考过程。

我在 Python 中制作了命令式版本,但您的答案不一定是在 Python 中; haskell 或任何其他语言也可以。

这是它的作用(将字符串作为输入并返回一个字符串):

"foo(a.d, b.e.fi()).go(sd, ds())"     -- returns -->  "foo().go()"
"foo(a, b).bar().fuu"                 -- returns -->  "foo().bar().fuu"
"foo.bar"                             -- returns -->  "foo.bar"

这是我的命令式代码:

def get_rid_of_arguments(text):
    i, start, end = 0, 0, 0
    result = ""
    for j, c in enumerate(text):
        if c == '(':
            if i == 0:
                start = j
                result += text[end:start]
            i += 1
        elif c == ')':
            i -= 1
            if i == 0:
                end = j + 1
                result += '()'
    return result + text[end:]

【问题讨论】:

  • 以下内容可能对您有所帮助。它处理一个不同但相对简单的问题。 neilmitchell.blogspot.ie/2013/09/…
  • 您想为实际函数执行此操作还是只为表示该函数的文本执行此操作?
  • @DiegoNolan 就在一个代表函数的字符串上。

标签: python haskell functional-programming imperative-programming


【解决方案1】:

这是我的版本:

import Control.Monad
import Control.Monad.State

-- filter `str` with the "stateful" monadic predicate function `handleChar`, 
-- with an initial state of 0
getRidOfArguments :: String -> String
getRidOfArguments str = filterM handleChar str `evalState` 0

handleChar :: Char -> State Int Bool
handleChar '(' = modify (+1) >> gets (<= 1)
handleChar ')' = modify (max 0 . subtract 1) >> gets (== 0)
handleChar _   = gets (== 0)

我的想法是:我们正在过滤一个列表,所以我想到了filter;然而,我们是否保留或删除一个字符取决于某些状态(我们打开/关闭括号的计数)。所以单子过滤函数filterM 是合适的,我们可以使用State 单子来抽象我们打开/关闭计数的管道。

如果您想了解有关上述工作原理的更多详细信息,请告诉我。

【讨论】:

  • 哦,这就是 monad 的用途。由于我不是haskell 专家,如果有没有单子的更简单的解决方案,我会更喜欢。谢谢你的回答。
  • 我可以建议(min 0 . subtract 1),而不是第二种情况下的那个 lambda
  • 我将此标记为已接受的答案,因为这似乎是要走的路。你也很好地解释了你的思考过程。不过,我认为 Changaco 的解决方案更容易理解,但当我更熟悉 monad 时,这可能会改变。
  • @jozefg 好主意,已编辑(尽管我认为您的意思是 max :))
  • @Zinggi 是的,考虑到我认为这个更抽象的“花哨”解决方案是最好的;它可能显示了 FP 如何准确地明确解决方案的哪些部分是“有状态的”或“有效的”(手动术语),并允许您将这些部分抽象出来。状态单子一开始很难理解(虽然不会使用);有些人发现我写的这个旧教程很有帮助:brandon.si/code/the-state-monad-a-tutorial-for-the-confused
【解决方案2】:

好吧,我更喜欢 jberryman 的解决方案,但如果你想避免使用 monad,试试这个

stateFilter :: (s -> a -> (s, Bool)) -> [a] -> s -> [a]
stateFilter f as state = snd $ foldr stepper (state, []) as
  where stepper (state, filtered) a =
          let (state', b) = f state a in
             if b then (state', a:filtered) else (state', filtered)

这通过我们的过滤函数保持状态运行,我们只返回当前值是否为真以及我们的新状态。那么你的代码就是

-- # Converted from jberrymans lovely answer
handleChar :: Int -> Char -> (Int, Bool)
handleChar s '(' = (max 0 (s - 1), s <= 1)
handleChar s ')' = (s +1, s <= 0)
handleChar s _   = (s, s == 0)

现在状态是明确的(并且不那么漂亮)但可能更容易理解。

clean str = stateFilter handleChar str 0

现在这很好而且很实用,整个事情归结为折叠字符串。有一些管道可以跟踪状态,但是一旦你开始更多地了解 Haskell,这就会很好地消失。

【讨论】:

    【解决方案3】:

    已经有很多答案了,但只是为了添加到列表中,这里有一个非常简单的功能风格。

    它使用一个辅助函数来计算嵌套计数。因此,0 表示不在括号内,1 表示在 1 对内等。如果 n > 0,则我们删除字符。如果我们相应地点击一个括号递增/递减 n。

    辅助函数基本上是对该算法的逐个描述。如果真的使用它,你会把它挂在“where”子句上。

    skipBrackets :: String -> String
    skipBrackets s = skipper s 0
    
    skipper :: String -> Int -> String
    
    skipper [] _ = []
    skipper ('(':xs) 0 = '(' : skipper xs 1
    skipper (')':xs) 1 = ')' : skipper xs 0
    
    skipper ('(':xs) n = skipper xs (n + 1)
    skipper (')':xs) n = skipper xs (n - 1)
    
    skipper (x:xs) 0 = x : skipper xs 0
    skipper (x:xs) n = skipper xs n
    

    【讨论】:

    • +1,另一个很好的从迭代到递归转换的演示。在这种情况下并不真正需要跟踪级别,但在其他情况下它很有用。但是,当降低一个级别时,您应该处理它变为负数的可能性,您的代码不会,因此在括号不匹配时会非常严重地失败。
    • 我真的很喜欢这个,这是迄今为止最简单的解决方案,谢谢。实际上,在阅读了所有答案后,我也想出了完全相同的解决方案;)
    • @Zinggi,我只有两种类型的功能脚本 - 简单且不起作用。
    【解决方案4】:

    一种方法是将迭代样式转换为递归样式。换句话说,不是使用for 循环多次执行某些代码,而是通过让函数调用自身来实现相同的目的。

    Haskell 中的一个例子:

    get_rid_of_arguments [] = []
    get_rid_of_arguments ('(':xs) = "()" ++ (get_rid_of_arguments $ dropper xs)
    get_rid_of_arguments (x:xs) = x : get_rid_of_arguments xs
    
    dropper [] = []
    dropper (')':xs) = xs
    dropper ('(':xs) = dropper $ dropper xs
    dropper (_:xs) = dropper xs
    
    main = do
        print $ get_rid_of_arguments "foo(a.d, b.e.fi()).go(sd, ds())" == "foo().go()"
        print $ get_rid_of_arguments "foo(a, b).bar().fuu" == "foo().bar().fuu"
        print $ get_rid_of_arguments "foo.bar" == "foo.bar"
    

    附:您的原始 python 代码和这个 Haskell 代码都不是“从代码的 sn-p 中去除函数参数”的正确方法,我只是在回答“我如何翻译这段代码”的问题。

    【讨论】:

    • 我觉得这个说的很清楚很容易理解,干得好!但是你说的不正确是什么意思?
    • @Zinggi 这是不正确的,因为它不能正确处理所有情况。例如,如果括号的数量是奇数,则两个函数都不会报告错误。另一个问题是它们会在括号之间删除所有内容,即使它实际上不是函数参数。理想情况下,您会使用一个真正的解析器,它会生成一个您可以操作的抽象语法树。
    • 你是对的,感谢您指出这一点。在我的情况下,括号是匹配的,我实际上想去掉括号之间的所有内容。我想我没有正确描述算法的目的。
    【解决方案5】:

    在进行这种转换时我喜欢的一个技巧是将尾调用视为一种 goto+变量赋值:

    sumn n = addn n 0
    
    addn i acc =
       if i == 0 then
          acc
       else
          addn (i-1) (acc + i)
    
    def sumn(n):
      #lets pretend Python has gotos for now...
      i, acc = n, 0
     acc:
      if i == 0:
         return acc
      else:
         i, acc = (i-1), (acc + i)
         goto acc
    

    在你的情况下,这会翻译成类似

    --Haskell pseudocode w/ string slicing
    get_rid xs = go 0 0 0 0 ""
      where
        -- "go" is a common name for these tail-recursive loop functions.
        go i j start end result =
          if j < length xs then
             case xs !! j of
               '('  -> 
                  if i == 0 then
                    go (i+1) (j+1) j end (result ++ (text[end:start]))
                  else
                    go (i+1) (j+1) start end result
               ')' -> 
                  if i == 1 then
                    go (i-1) (j+1) start (j+1) (result ++ "()")
                  else
                    go (i-1) (j+1) start end result
               _ ->
                  go i (j+1) start end result
          else
             result ++ text[end:]
    

    最终结果非常丑陋,因为这仍然是一个基本的命令式算法,需要进行大量的变量赋值。此外,功能版本明确表明您的所有变量都在可用的最大范围内(“go”循环):我想您应该能够通过使用内部循环来摆脱“start”和“end”来查找下一个 ")" 而不是在主循环中执行所有操作(这对原始 Python 程序也有效)。

    还有一些小的样式问题,比如仍然在链表上使用索引和切片(它在 Haskell 中的 O(N) 而不是 O(1))以及使用尾递归循环 (gotos) 而不是更结构化的折叠(用于循环)。也就是说,重要的一点是,如果您真的愿意,您仍然可以编写算法的原始、命令式版本。

    【讨论】:

    • 嗯,这看起来更像是 c,然后是一个优雅的函数式解决方案 ;) 但是你证明了你的观点,你可以一步一步轻松地翻译算法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-11
    • 2018-03-13
    • 2011-09-30
    • 1970-01-01
    • 2021-01-13
    • 2011-03-20
    • 2016-01-08
    相关资源
    最近更新 更多