Lập trình hàm và thuật toán trạng thái


12

Tôi đang học lập trình chức năng với Haskell . Trong khi đó, tôi đang học lý thuyết Automata và khi cả hai dường như rất hợp nhau, tôi đang viết một thư viện nhỏ để chơi với automata.

Đây là vấn đề khiến tôi phải đặt câu hỏi. Trong khi nghiên cứu một cách để đánh giá khả năng tiếp cận của một trạng thái, tôi có ý tưởng rằng một thuật toán đệ quy đơn giản sẽ khá kém hiệu quả, bởi vì một số đường dẫn có thể chia sẻ một số trạng thái và cuối cùng tôi có thể đánh giá chúng nhiều lần.

Ví dụ, ở đây, đánh giá khả năng tiếp cận của g từ a , tôi phải loại trừ f cả khi kiểm tra con đường thông qua dc :

máy in đại diện cho một máy tự động

Vì vậy, ý tưởng của tôi là một thuật toán hoạt động song song trên nhiều đường dẫn và cập nhật một bản ghi được chia sẻ về các trạng thái bị loại trừ có thể là tuyệt vời, nhưng đó là quá nhiều đối với tôi.

Tôi đã thấy rằng trong một số trường hợp đệ quy đơn giản, người ta có thể vượt qua trạng thái như một đối số, và đó là điều tôi phải làm ở đây, bởi vì tôi chuyển tiếp danh sách các trạng thái tôi đã trải qua để tránh các vòng lặp. Nhưng có cách nào để vượt qua danh sách đó ngược lại, như trả lại nó trong một tuple cùng với kết quả boolean của canReachhàm của tôi không? (mặc dù điều này cảm thấy một chút gượng ép)

Bên cạnh tính hợp lệ của trường hợp ví dụ của tôi , còn có những kỹ thuật nào khác để giải quyết loại vấn đề này? Tôi cảm thấy như những điều này phải đủ phổ biến để có những giải pháp như những gì xảy ra với fold*hoặc map.

Cho đến nay, đọc learnyouahaskell.com tôi vẫn chưa tìm thấy, nhưng xem xét tôi chưa chạm vào các đơn nguyên.

( nếu quan tâm, tôi đã đăng mã của mình lên codereview )


3
Tôi, đối với một người, rất thích xem mã mà bạn đã cố gắng làm việc với. Trong trường hợp không có điều đó, lời khuyên tốt nhất của tôi là sự lười biếng của Haskell thường có thể được khai thác để không tính toán mọi thứ nhiều hơn một lần. Nhìn vào cái gọi là "buộc nút" và đệ quy giá trị lười biếng, mặc dù vấn đề của bạn có thể đủ đơn giản để các kỹ thuật tiên tiến hơn tận dụng các giá trị vô hạn và những thứ tương tự sẽ trở nên quá mức, và có lẽ sẽ khiến bạn bối rối ngay bây giờ.
Ngọn lửa của Ptharien

1
@ Ptharien'sFlame cảm ơn bạn đã quan tâm! Đây là mã , cũng có một liên kết đến toàn bộ dự án. Tôi đã bối rối với những gì tôi đã làm cho đến nay, vì vậy, tốt hơn là không nên tìm hiểu các kỹ thuật tiên tiến :)
bigstones

1
Một automata nhà nước là khá nhiều phản đề của lập trình chức năng. Lập trình chức năng là giải quyết các vấn đề mà không có trạng thái bên trong, trong khi một automata trạng thái là tất cả về việc quản lý trạng thái của chính nó.
Philipp

@Philipp tôi không đồng ý. Máy tự động hoặc máy trạng thái đôi khi là cách tự nhiên và chính xác nhất để thể hiện vấn đề và automata chức năng được nghiên cứu kỹ lưỡng.
Ngọn lửa của Ptharien

5
@Philipp: lập trình chức năng là về việc làm cho nhà nước rõ ràng, không phải về việc cấm nó. Trong thực tế, đệ quy đuôi là một công cụ thực sự tuyệt vời để thực hiện các máy trạng thái đầy gotos.
hugomg

Câu trả lời:


16

Lập trình chức năng không thoát khỏi trạng thái. Nó chỉ làm cho nó rõ ràng! Mặc dù đúng là các chức năng như bản đồ thường sẽ "làm sáng tỏ" cấu trúc dữ liệu "được chia sẻ", nhưng nếu tất cả những gì bạn muốn làm là viết một thuật toán có thể truy cập thì đó chỉ là vấn đề theo dõi các nút bạn đã truy cập:

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]

Bây giờ, tôi phải thú nhận rằng việc theo dõi tất cả trạng thái này bằng tay khá khó chịu và dễ bị lỗi (thật dễ dàng để sử dụng s 'thay vì s' ', thật dễ dàng để chuyển cùng một s' cho nhiều hơn một tính toán ...) . Đây là nơi các đơn vị xuất hiện: họ không thêm bất cứ điều gì bạn chưa từng làm trước đây nhưng họ cho phép bạn vượt qua biến trạng thái xung quanh và giao diện đảm bảo rằng điều đó xảy ra theo cách đơn luồng.


Chỉnh sửa: Tôi sẽ cố gắng đưa ra lý do nhiều hơn về những gì tôi đã làm bây giờ: trước hết, thay vì chỉ kiểm tra khả năng tiếp cận, tôi đã mã hóa một tìm kiếm theo chiều sâu. Việc thực hiện sẽ trông khá giống nhau nhưng việc gỡ lỗi có vẻ tốt hơn một chút.

Trong một ngôn ngữ có trạng thái, DFS sẽ trông giống như thế này:

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)

Bây giờ chúng ta cần tìm cách thoát khỏi trạng thái đột biến. Trước hết, chúng tôi loại bỏ biến "danh sách truy cập" bằng cách làm cho dfs trả về giá trị đó thay vì void:

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))

Và bây giờ đến phần khó khăn: loại bỏ biến "đã truy cập". Thủ thuật cơ bản là sử dụng một quy ước trong đó chúng ta chuyển trạng thái như một tham số bổ sung cho các hàm cần nó và để các hàm đó trả về phiên bản mới của trạng thái như một giá trị trả về bổ sung nếu chúng muốn sửa đổi nó.

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...

Để áp dụng mẫu này cho các dfs, chúng ta cần thay đổi nó để nhận được "lượt truy cập" được đặt làm tham số bổ sung và trả về phiên bản cập nhật của "đã truy cập" dưới dạng giá trị trả về bổ sung. Ngoài ra, chúng tôi cần viết lại mã để chúng tôi luôn chuyển tiếp phiên bản "gần đây nhất" của mảng "đã truy cập":

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)

Phiên bản Haskell thực hiện khá nhiều những gì tôi đã làm ở đây, ngoại trừ việc nó đi tất cả các cách và sử dụng một hàm đệ quy bên trong thay vì các biến "current_visited" và "childtrees" có thể thay đổi.


Đối với các đơn nguyên, những gì họ thực hiện về cơ bản là hoàn toàn vượt qua "dòng chảy" xung quanh, thay vì buộc bạn phải làm điều đó bằng tay. Điều này không chỉ loại bỏ sự lộn xộn khỏi mã mà còn ngăn bạn mắc lỗi, chẳng hạn như trạng thái cưỡng bức (chuyển cùng một "lượt truy cập" được đặt thành hai cuộc gọi tiếp theo thay vì xâu chuỗi trạng thái).


Tôi biết rằng phải có một cách để làm cho nó bớt đau đớn hơn, và có thể dễ đọc hơn, bởi vì tôi đang gặp khó khăn trong việc hiểu ví dụ của bạn. Tôi có nên đi cho các đơn nguyên hoặc thực hành tốt hơn để hiểu mã như của bạn?
bigstones

@bigstones: Tôi nghĩ bạn nên cố gắng hiểu cách mã của tôi hoạt động trước khi giải quyết các đơn nguyên - về cơ bản chúng sẽ làm điều tương tự tôi đã làm nhưng có thêm các lớp trừu tượng để làm bạn bối rối. Dù sao, tôi đã thêm một số lời giải thích thêm để cố gắng làm cho mọi thứ rõ ràng hơn một chút
hugomg

1
"Lập trình chức năng không thoát khỏi trạng thái. Nó chỉ làm cho nó rõ ràng!": Điều này thực sự rõ ràng!
Giorgio

"[Monads] ​​cho phép bạn vượt qua biến trạng thái xung quanh và giao diện đảm bảo rằng nó xảy ra theo cách đơn luồng" <- Đây là mô tả rõ ràng về các đơn nguyên; ngoài ngữ cảnh của câu hỏi này, tôi có thể thay thế 'biến trạng thái' bằng 'đóng'
anthropic android

2

Đây là một câu trả lời đơn giản dựa vào 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

Trường hợp neighborstrả về các trạng thái ngay lập tức kết nối với một trạng thái. Điều này trả về một loạt các đường dẫn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.