การเขียนโปรแกรมการทำงานและอัลกอริทึม stateful


12

ฉันเรียนรู้การเขียนโปรแกรมการทำงานที่มีHaskell ในขณะเดียวกันฉันกำลังศึกษาทฤษฎีออโตมาตาและในขณะที่ทั้งสองดูเหมือนจะเข้ากันได้ดีฉันจึงเขียนห้องสมุดเล็ก ๆ เพื่อเล่นกับออโตมาตะ

นี่คือปัญหาที่ทำให้ฉันถามคำถาม ในขณะที่ศึกษาวิธีประเมินความสามารถในการเข้าถึงของรัฐฉันได้รับแนวคิดว่าอัลกอริทึมแบบเรียกซ้ำง่าย ๆ จะไม่มีประสิทธิภาพมากนักเนื่องจากบางเส้นทางอาจแบ่งปันบางสถานะและฉันอาจจบการประเมินมากกว่าหนึ่งครั้ง

ตัวอย่างเช่นที่นี่การประเมินความสามารถในการเข้าถึงของgจากaฉันต้องแยกfทั้งสองขณะตรวจสอบเส้นทางผ่านdและc :

digraph เป็นตัวแทนของหุ่นยนต์

ดังนั้นความคิดของฉันก็คืออัลกอริทึมทำงานแบบขนานบนหลาย ๆ พา ธ และอัปเดตบันทึกที่แชร์ของรัฐที่ถูกแยกอาจจะดี แต่นั่นก็มากเกินไปสำหรับฉัน

ฉันเคยเห็นว่าในกรณีการเรียกซ้ำง่าย ๆ บางครั้งเราสามารถผ่านสถานะเป็นอาร์กิวเมนต์และนั่นคือสิ่งที่ฉันต้องทำที่นี่เพราะฉันส่งต่อรายชื่อของรัฐที่ฉันได้ผ่านเพื่อหลีกเลี่ยงการวนซ้ำ แต่มีวิธีที่จะส่งผ่านรายการนั้นย้อนกลับเช่นกลับใน tuple พร้อมกับผลบูลีนของcanReachฟังก์ชั่นของฉันได้อย่างไร (แม้ว่าจะรู้สึกว่าถูกบังคับเล็กน้อย)

นอกจากความถูกต้องของตัวอย่างคดีของฉันแล้วยังมีเทคนิคอื่นใดอีกบ้างที่สามารถใช้แก้ปัญหาแบบนี้ได้อีก? ฉันรู้สึกเหมือนเหล่านี้จะต้องมากพอที่พบว่ามีจะต้องมีการแก้ปัญหาเช่นสิ่งที่เกิดขึ้นด้วยหรือfold*map

จนถึงตอนนี้การอ่านlearnyouahaskell.comฉันยังไม่พบอะไรเลย แต่ลองคิดดูว่าฉันยังไม่ได้แตะพระเลย

( ถ้าสนใจฉันโพสต์รหัสของฉันในcodereview )


3
ฉันอยากจะเห็นรหัสที่คุณพยายามจะทำงานด้วย ในกรณีที่ไม่มีคำแนะนำที่ดีที่สุดของฉันคือความเกียจคร้านของ Haskell มักถูกนำไปใช้เพื่อไม่คำนวณสิ่งต่าง ๆ มากกว่าหนึ่งครั้ง ตรวจสอบสิ่งที่เรียกว่า "การผูกปม" และการเรียกซ้ำค่าที่ขี้เกียจแม้ว่าปัญหาของคุณน่าจะง่ายพอที่เทคนิคขั้นสูงกว่าที่ใช้ประโยชน์จากค่าที่ไม่มีที่สิ้นสุดและสิ่งที่คล้ายกันจะ overkill และอาจทำให้สับสน
Flame ของ Ptharien

1
@ Ptharien'sFlame ขอบคุณที่ให้ความสนใจ! นี่คือรหัสนอกจากนี้ยังมีลิงก์ไปยังโครงการทั้งหมด ฉันสับสนแล้วกับสิ่งที่ฉันได้ทำไปแล้วดังนั้นใช่ดีกว่าที่จะไม่ดูเป็นเทคนิคขั้นสูง :)
bigstones

1
ออโตมาเตตของรัฐนั้นค่อนข้างตรงกันข้ามกับการเขียนโปรแกรมเชิงฟังก์ชัน ฟังก์ชั่นการเขียนโปรแกรมเป็นเรื่องเกี่ยวกับการแก้ปัญหาโดยไม่มีสถานะภายในในขณะที่สถานะออโตมาตาเป็นเรื่องเกี่ยวกับการจัดการสถานะของตัวเอง
ฟิลิปป์

@ ฟิลิปฉันไม่เห็นด้วย เครื่องออโตเมติกหรือเครื่องจักรกลรัฐเป็นบางครั้งวิธีที่เป็นธรรมชาติและแม่นยำที่สุดในการเป็นตัวแทนของปัญหา
Flame ของ Ptharien

5
@ ฟิลลิปป์: การเขียนโปรแกรมฟังก์ชั่นเป็นเรื่องเกี่ยวกับการทำให้รัฐชัดเจนไม่เกี่ยวกับการห้ามมัน ในความเป็นจริงการเรียกซ้ำแบบหางเป็นเครื่องมือที่ยอดเยี่ยมสำหรับการใช้งานเครื่องรัฐที่เต็มไปด้วย gotos
hugomg

คำตอบ:


16

การเขียนโปรแกรมฟังก์ชั่นไม่ได้กำจัดรัฐ มันทำให้ชัดเจนเท่านั้น! ในขณะที่มันเป็นความจริงที่ฟังก์ชั่นเช่นแผนที่มักจะ "คลี่คลาย" โครงสร้างข้อมูล "ที่ใช้ร่วมกัน" หากคุณต้องการทำคือการเขียนอัลกอริธึมการเข้าถึงได้มันเป็นเพียงเรื่องของการติดตามว่าโหนดใดที่คุณเยี่ยมชมแล้ว

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' 'มันง่ายที่จะส่งการคำนวณเดียวกันมากกว่าหนึ่งการคำนวณ ... ) . นี่คือที่มาของพระ: พวกเขาไม่ได้เพิ่มอะไรที่คุณไม่เคยทำมาก่อน แต่พวกเขาให้คุณผ่านตัวแปรสถานะรอบ ๆ โดยปริยายและอินเทอร์เฟซรับประกันว่ามันจะเกิดขึ้นในแบบเธรดเดียว


แก้ไข: ฉันจะพยายามให้เหตุผลเพิ่มเติมเกี่ยวกับสิ่งที่ฉันทำตอนนี้: ก่อนอื่นแทนที่จะแค่ทดสอบความสามารถในการเข้าถึงที่ฉันเขียนโค้ดการค้นหาเชิงลึกครั้งแรก การนำไปใช้จะดูค่อนข้างเหมือนเดิม แต่การดีบั๊กดูดีขึ้นเล็กน้อย

ในภาษา stateful, 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 คืนค่านั้นแทน 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))

และตอนนี้มาถึงส่วนที่ยุ่งยาก: กำจัดตัวแปร "เยี่ยมชม" เคล็ดลับพื้นฐานคือการใช้การประชุมที่เราผ่านรัฐเป็นพารามิเตอร์เพิ่มเติมไปยังฟังก์ชั่นที่ต้องการและมีฟังก์ชั่นเหล่านั้นกลับรุ่นรัฐใหม่เป็นค่าตอบแทนพิเศษหากพวกเขาต้องการแก้ไข

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" ไปโดยปริยายแทนที่จะบังคับให้คุณทำมันด้วยมือ ไม่เพียง แต่จะลบความยุ่งเหยิงออกจากรหัสเท่านั้น แต่ยังป้องกันไม่ให้คุณทำผิดพลาดเช่นการฟอร์กสถานะ


ฉันรู้ว่าต้องมีวิธีที่จะทำให้มันเจ็บปวดน้อยลงและอาจจะอ่านง่ายขึ้นเพราะฉันมีเวลายากที่จะเข้าใจตัวอย่างของคุณ ฉันควรไปที่ monads หรือฝึกฝนให้ดีขึ้นเพื่อทำความเข้าใจกับรหัสเช่นคุณหรือไม่?
bigstones

@bigstones: ฉันคิดว่าคุณควรพยายามเข้าใจวิธีการใช้งานโค้ดของฉันก่อนที่จะแก้ปัญหา monads - โดยทั่วไปแล้วพวกเขาจะทำสิ่งเดียวกันกับที่ฉันทำ อย่างไรก็ตามฉันได้เพิ่มคำอธิบายพิเศษเพื่อพยายามทำให้สิ่งต่าง ๆ ชัดเจนขึ้น
hugomg

1
"การเขียนโปรแกรมฟังก์ชั่นไม่กำจัดรัฐมันทำให้ชัดเจนเท่านั้น!": นี่เป็นการชี้แจงที่ชัดเจนจริงๆ!
Giorgio

"[Monads] ​​ให้คุณผ่านตัวแปรสถานะรอบ ๆ โดยปริยายและอินเทอร์เฟซรับประกันว่ามันจะเกิดขึ้นในลักษณะเธรดเดียว" <- นี่คือคำอธิบายที่กระจ่างของ monads; นอกบริบทของคำถามนี้ฉันอาจแทนที่ 'ตัวแปรรัฐ' ด้วย 'ปิด'
Android anthropic

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ผลตอบแทนรัฐที่เชื่อมต่อทันทีเพื่อรัฐ ส่งคืนชุดของพา ธ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.