Функціональне програмування та алгоритми стану


12

Я вивчаю функціональне програмування з Haskell . Тим часом я вивчаю теорію автоматів, і як вони, здається, добре поєднуються разом, я пишу невеличку бібліотеку, щоб пограти з автоматами.

Ось проблема, яка змусила мене задати питання. Під час вивчення способу оцінки досяжності стану я зрозумів, що простий рекурсивний алгоритм буде досить неефективним, оскільки деякі шляхи можуть поділяти деякі стани, і я можу в кінцевому підсумку оцінити їх не один раз.

Наприклад, тут, оцінка досяжності з г від , я мав би виключити е і при перевірці шляху через D і C :

диграф, що представляє автомат

Тож моя ідея полягає в тому, що алгоритм, що працює паралельно на багатьох шляхах і оновлює спільний запис виключених станів, може бути чудовим, але це для мене занадто багато.

Я бачив, що в деяких простих випадках рекурсії можна передати стан як аргумент, і це те, що я маю робити тут, тому що я передаю вперед список держав, через які я пройшов, щоб уникнути циклів. Але чи є спосіб передати цей список також назад, як повернути його в кортежі разом із булевим результатом моєї canReachфункції? (хоча це здається трохи вимушеним)

Окрім обґрунтованості мого прикладу , які інші методи є для вирішення подібних проблем? Я відчуваю, що вони повинні бути досить поширеними, що повинні бути такі рішення, як те, що відбувається з fold*або map.

Поки, читаючи learnnyouahaskell.com, я не знайшов жодного, але вважаю, що ще не торкнувся монад.

( якщо цікавить, я розмістив свій код на перегляді коду )


3
Я, наприклад, хотів би побачити код, з яким ви намагалися працювати. За відсутності цього, моя найкраща порада полягає в тому, що лінь Хаскелла часто можна використовувати, щоб не обчислювати речі більше одного разу. Погляньте на так звану "зав'язування вузла" та рецидиву ледачої величини, хоча ваша проблема, ймовірно, досить проста, що більш досконалі методи, які використовують переваги нескінченних значень і подібних речей, будуть надмірними, і, ймовірно, просто заплутають вас зараз.
Полум’я Птарієна

1
@ Ptharien'sFlame дякую за інтерес! ось код , також є посилання на весь проект. Я вже плутаю те, що робив до цього часу, так що так, краще не заглядати в передові методики :)
bigstones

1
Державні автомати є в значній мірі антитезою функціонального програмування. Функціональне програмування - це вирішення проблем без внутрішнього стану, тоді як державні автомати - це все про управління власною державою.
Філіпп

@Philipp Я не згоден. Автомат або державна машина іноді є найбільш природним і точним способом представлення проблеми, а функціональні автомати добре вивчені.
Полум’я Птарієна

5
@Philipp: функціональне програмування полягає в тому, щоб зробити стан явним, а не забороняти його. Насправді хвостова рекурсія - це дійсно чудовий інструмент для впровадження цих державних машин, повних готосів.
hugomg

Відповіді:


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' на кілька обчислень ...) . Ось тут і приходять монади: вони не додають нічого, чого ви раніше не могли зробити, але дозволяють вам передавати змінну стану навколо неявно, а інтерфейс гарантує, що це відбувається однопотоково.


Редагувати: Я спробую дати більше міркувань того, що я робив зараз: насамперед, замість того, щоб просто перевірити на доступність, я зашифрував пошук по глибині. Реалізація буде виглядати приблизно так само, але налагодження виглядає трохи краще.

Державною мовою 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" навколо, а не змушення вас робити це вручну. Це не тільки видаляє безлад з коду, але і заважає вам помилитися, наприклад, розпізнавання стану (передача того ж «відвіданого» встановленого на два наступні дзвінки замість прив’язування стану).


Я знав, що повинен бути спосіб зробити його менш болісним і, можливо, більш читабельним, бо мені важко зрозуміти ваш приклад. Чи варто піти на монади чи краще практикувати більше, щоб зрозуміти код, як ваш?
bigstones

@bigstones: Я думаю, ви повинні спробувати зрозуміти, як працює мій код, перш ніж боротися з монадами - вони в основному будуть робити те саме, що я зробив, але з додатковими шарами абстракції, щоб вас бентежити. У всякому разі, я додав додаткове пояснення, щоб спробувати зробити речі яснішими
hugomg

1
"Функціональне програмування не позбавляється від стану. Це лише робить це явним!": Це дійсно з'ясовується!
Джорджіо

"[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

Де neighborsповертає штати, негайно пов'язані зі станом. Це повертає ряд шляхів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.