Функціональне програмування не позбавляється від стану. Це лише робить це явним! Хоча це правда, що такі функції, як 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' на кілька обчислень ...) . Ось тут і приходять монади: вони не додають нічого, чого ви раніше не могли зробити, але дозволяють вам передавати змінну стану навколо неявно, а інтерфейс гарантує, що це відбувається однопотоково.
Редагувати: Я спробую дати більше міркувань того, що я робив зараз: насамперед, замість того, щоб просто перевірити на доступність, я зашифрував пошук по глибині. Реалізація буде виглядати приблизно так само, але налагодження виглядає трохи краще.
Державною мовою 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)
Тепер нам потрібно знайти спосіб позбутися від стану, що змінюється. Перш за все, ми позбавляємось змінної "visitlist", роблячи повернення dfs, що замість недійсного:
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, нам потрібно змінити його, щоб отримати набір "відвідав" як додатковий параметр і повернути оновлену версію "відвідав" як додаткове повернене значення. Крім того, нам потрібно переписати код, щоб ми завжди передавали вперед "останню" версію "відвіданого" масиву:
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" навколо, а не змушення вас робити це вручну. Це не тільки видаляє безлад з коду, але і заважає вам помилитися, наприклад, розпізнавання стану (передача того ж «відвіданого» встановленого на два наступні дзвінки замість прив’язування стану).