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