功能编程和状态算法


12

我正在用Haskell学习函数式编程。同时,我正在研究自动机理论,由于两者看起来很融洽,所以我正在编写一个小型库来玩自动机。

这就是让我问的问题。在研究评估状态可达性的方法时,我想到了一个简单的递归算法效率不高,因为某些路径可能共享某些状态,而我可能最终不只一次评估它们。

例如,在这里,评估可达性一个,我必须排除˚F双方同时检查通过路径dÇ

有向图代表自动机

因此,我的想法是,在多个路径上并行工作并更新排除状态的共享记录的算法可能很棒,但这对我来说太过分了。

我已经看到,在某些简单的递归情况下,可以将状态作为参数传递,这就是我在这里要做的,因为我向前传递了要避免循环的状态列表。但是是否有一种方法也可以向后传递该列表,例如将其与canReach函数的布尔结果一起返回到元组中?(尽管这有点强迫)

除了示例案例的有效性之外,还有哪些其他技术可用来解决此类问题?我觉得这些必须足够普遍,以至于必须有解决方案,例如fold*or 会发生什么map

到目前为止,阅读learningyouahaskell.com并没有发现任何内容,但考虑到我还没有接触过monad。

如果有兴趣,我将代码发布在codereview上


3
我(其中之一)很乐意看到您一直尝试使用的代码。在没有这种情况的情况下,我最好的建议是,Haskell的惰性可以经常被利用来进行多次计算。研究所谓的“打结”和懒惰的值递归,尽管您的问题可能很简单,以至于利用无限值和类似事物的更先进的技术可能会过时,并且可能现在使您感到困惑。
Ptharien's Flame

1
@ Ptharien'sFlame感谢您的关注!这是代码,还有指向整个项目的链接。我已经对自己到目前为止所做的事情感到困惑,是的,最好不要研究先进的技术:)
bigstones

1
状态自动机几乎是功能编程的对立面。函数式编程是在没有内部状态的情况下解决问题,而状态自动机则是在管理自己的状态。
菲利普

@Philipp我不同意。自动机或状态机有时是表示问题的最自然,最准确的方法,并且对功能自动机的研究也很深入。
Ptharien's Flame

5
@Philipp:函数式编程是要使状态显式,而不是禁止状态。实际上,尾部递归对于实现那些充满了goto的状态机是一个非常好的工具。
hugomg 2013年

Answers:


16

函数式编程不会摆脱状态。它只是使它明确!确实,像map这样的函数通常会“分解”“共享”数据结构,但是,如果您要做的只是编写可及性算法,那么只需跟踪已访问的节点即可:

import qualified Data.Set as S
data Node = Node Int [Node] deriving (Show)

-- Receives a root node, returns a list of the node keyss visited in a depth-first search
dfs :: Node -> [Int]
dfs x = fst (dfs' (x, S.empty))

-- This worker function keeps track of a set of already-visited nodes to ignore.
dfs' :: (Node, S.Set Int) -> ([Int], S.Set Int)
dfs' (node@(Node k ns), s )
  | k  `S.member` s = ([], s)
  | otherwise =
    let (childtrees, s') = loopChildren ns (S.insert k s) in
    (k:(concat childtrees), s')

--This function could probably be implemented as just a fold but Im lazy today...
loopChildren :: [Node] -> S.Set Int -> ([[Int]], S.Set Int)
loopChildren []  s = ([], s)
loopChildren (n:ns) s =
  let (xs, s') = dfs' (n, s) in
  let (xss, s'') = loopChildren ns s' in
  (xs:xss, s'')

na = Node 1 [nb, nc, nd]
nb = Node 2 [ne]
nc = Node 3 [ne, nf]
nd = Node 4 [nf]
ne = Node 5 [ng]
nf = Node 6 []
ng = Node 7 []

main = print $ dfs na -- [1,2,5,7,3,6,4]

现在,我必须承认手动跟踪所有这些状态非常烦人并且容易出错(使用s'而不是s''很容易,将相同的s'传递给多个计算很容易...) 。这是monad进入的地方:它们不添加以前无法完成的任何事情,但是它们使您可以隐式传递状态变量,并且接口保证它以单线程方式发生。


编辑:我将尝试给出我现在所做的更多推理:首先,我对深度优先搜索进行了编码,而不仅仅是对可达性的测试。实现看起来几乎相同,但是调试看起来更好一些。

用有状态语言,DFS看起来像这样:

visited = set()  #mutable state
visitlist = []   #mutable state
def dfs(node):
   if isMember(node, visited):
       //do nothing
   else:
       visited[node.key] = true           
       visitlist.append(node.key)
       for child in node.children:
         dfs(child)

现在我们需要找到一种摆脱可变状态的方法。首先,我们通过使dfs返回而不是void来摆脱“ visitlist”变量:

visited = set()  #mutable state
def dfs(node):
   if isMember(node, visited):
       return []
   else:
       visited[node.key] = true
       return [node.key] + concat(map(dfs, node.children))

现在最棘手的部分是:摆脱“访问”变量。基本技巧是使用一种约定,在该约定中,我们将状态作为附加参数传递给需要状态的函数,并让这些函数在希望修改状态时将状态的新版本作为附加返回值返回。

let increment_state s = s+1 in
let extract_state s = (s, 0) in

let s0 = 0 in
let s1 = increment_state s0 in
let s2 = increment_state s1 in
let (x, s3) = extract_state s2 in
-- and so on...

要将这种模式应用于dfs,我们需要对其进行更改,以接收作为额外参数的“ visited”设置,并作为额外的返回值返回“ visited”的更新版本。另外,我们需要重写代码,以便始终传递“ visited”数组的“最新”版本:

def dfs(node, visited1):
   if isMember(node, visited1):
       return ([], visited1) #return the old state because we dont want to  change it
   else:
       curr_visited = insert(node.key, visited1) #immutable update, with a new variable for the new value
       childtrees = []
       for child in node.children:
          (ct, curr_visited) = dfs(child, curr_visited)
          child_trees.append(ct)
       return ([node.key] + concat(childTrees), curr_visited)

Haskell版本几乎完成了我在这里所做的工作,只是它一直运行并使用内部递归函数,而不是可变的“ curr_visited”和“ childtrees”变量。


至于单子,它们基本完成的任务是隐式传递“ curr_visited”,而不是强迫您手动完成。这不仅可以消除代码中的混乱情况,而且还可以防止您出错,例如派生状态(将相同的“已访问”设置传递给两个后续调用,而不是链接状态)。


我知道必须有一种方法来减轻它的痛苦,甚至使其更具可读性,因为我很难理解您的示例。我应该参加monad还是更好地练习以理解像您一样的代码?
bigstones,2013年

@bigstones:我认为您应该在解决monad之前先了解我的代码是如何工作的-它们基本上会执行与我相同的操作,但是要加上额外的抽象层使您感到困惑。无论如何,我添加了一些额外的解释来尝试使事情变得更清晰
hugomg

1
“函数式编程不会摆脱状态。它只会使状态变得明确!”:这确实很清楚!
Giorgio 2014年

“ [Monads]​​允许您隐式传递状态变量,并且接口保证它以单线程方式发生。” <-这是对monads的说明。在此问题的上下文之外,我可以将“状态变量”替换为“关闭”
人类机器人系统,

2

这是一个简单的答案mapConcat

 mapConcat :: (a -> [b]) -> [a] -> [b]
 -- mapConcat is in the std libs, mapConcat = concat . map
 type Path = []

 isReachable :: a -> Auto a -> a -> [Path a]
 isReachable to auto from | to == from = [[]]
 isReachable to auto from | otherwise = 
    map (from:) . mapConcat (isReachable to auto) $ neighbors auto from

Where neighbors返回立即连接到状态的状态。这将返回一系列路径。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.