ความแตกต่างระหว่างการเรียกซ้ำและการสำรวจคืออะไร


55

ความแตกต่างระหว่างสิ่งเหล่านี้คืออะไร?

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

อะไรคือตัวอย่างง่ายๆที่อธิบายคำศัพท์เหล่านี้

การกลับเป็นสองเท่าของการเรียกซ้ำเป็นอย่างไร?

มีอัลกอริธึมการยืนยันคลาสสิกหรือไม่?


45
ดูคำตอบของ SO stackoverflow.com/questions/10138735/ … (ขออภัยไม่สามารถหยุดตัวเองได้)
เครื่องหมายประสิทธิภาพสูง

7
@ HighPerformanceMark มันไม่ได้อธิบายว่า corecursion คืออะไรเราจำเป็นต้องมีคำถามอีกข้อ
Abyx

5
แต่อย่างจริงจังมีอะไรผิดปกติกับคำอธิบาย Wikipedia ของข้อกำหนดเหล่านี้หรือไม่
เครื่องหมายประสิทธิภาพสูง

5
คำอธิบายการวิพากษ์วิจารณ์เกี่ยวกับวิกิพีเดียนั้นแย่มาก ฉันสงสัยว่ามันสมเหตุสมผลกับทุกคนที่ไม่รู้ว่าการสำรวจคืออะไร
Marcin

9
@ High Performance Mark: ฉันคลิกที่ลิงค์สามครั้งคิดว่ามีข้อผิดพลาดก่อนที่ฉันจะเข้าใจปุน LOL
Giorgio

คำตอบ:


24

มีหลายวิธีที่ดีในการดูที่นี้ สิ่งที่ง่ายที่สุดสำหรับฉันคือคิดถึงความสัมพันธ์ระหว่าง "Inductive" และ "Coinductive definitions"

นิยามอุปนัยของชุดไปเช่นนี้

ชุด "แน็ต" ถูกกำหนดให้เป็นชุดที่เล็กที่สุดเช่น "Zero" ที่อยู่ในแน็ตและถ้า n อยู่ในแน็ต "Succ n" อยู่ในแน็ต

ซึ่งสอดคล้องกับ Ocaml ต่อไปนี้

type nat = Zero | Succ of nat

สิ่งหนึ่งที่ควรทราบเกี่ยวกับคำจำกัดความนี้ก็คือตัวเลข

omega = Succ(omega)

ไม่ใช่สมาชิกของชุดนี้ ทำไม? สมมติว่ามันเป็นตอนนี้พิจารณาชุด N ที่มีองค์ประกอบเดียวกันทั้งหมดกับ Nat ยกเว้นว่ามันจะไม่มีโอเมก้า เห็นได้ชัดว่า Zero อยู่ใน N และถ้า y อยู่ใน N, Succ (y) อยู่ใน N แต่ N นั้นเล็กกว่าแน็ตซึ่งขัดแย้งกัน ดังนั้นโอเมก้าไม่ได้อยู่ในแน็ต

หรืออาจมีประโยชน์มากกว่าสำหรับนักวิทยาศาสตร์คอมพิวเตอร์:

ให้เซต "a" บางชุด "List of a" ถูกกำหนดเป็นชุดที่เล็กที่สุดที่ "Nil" อยู่ในรายการ a และถ้า xs อยู่ในรายการ a และ x อยู่ใน "Cons x xs" อยู่ในรายการของ

ซึ่งสอดคล้องกับสิ่งที่ชอบ

type 'a list = Nil | Cons of 'a * 'a list

คำผ่าตัดที่นี่คือ "เล็กที่สุด" ถ้าเราไม่พูดว่า "เล็กที่สุด" เราคงไม่มีทางบอกได้เลยว่ากลุ่มแน็ตบรรจุกล้วย!

อีกครั้ง

zeros = Cons(Zero,zeros)

ไม่ใช่คำจำกัดความที่ถูกต้องสำหรับรายการของ nats เช่นเดียวกับโอเมก้าไม่ใช่แน็ตที่ถูกต้อง

การกำหนดข้อมูล inductively เช่นนี้ช่วยให้เราสามารถกำหนดฟังก์ชั่นที่ทำงานบนโดยใช้การเรียกซ้ำ

let rec plus a b = match a with
                   | Zero    -> b
                   | Succ(c) -> let r = plus c b in Succ(r)

จากนั้นเราสามารถพิสูจน์ข้อเท็จจริงเกี่ยวกับสิ่งนี้เช่น "plus a Zero = a" โดยใช้การเหนี่ยวนำ (โดยเฉพาะการเหนี่ยวนำเชิงโครงสร้าง)

หลักฐานของเราดำเนินการโดยการเหนี่ยวนำโครงสร้างใน
สำหรับกรณีพื้นฐานให้เป็นศูนย์ เพื่อให้เรารู้plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r) plus Zero Zero = Zeroอนุญาตaเป็น nat plus a Zero = aสมมติสมมติฐานว่า ตอนนี้เราแสดงให้เห็นว่าplus (Succ(a)) Zero = Succ(a)สิ่งนี้ชัดเจนตั้งแต่plus (Succ(a)) Zero = match a with |Zero -> Zero | Succ(a) -> let r = plus a Zero in Succ(r) = let r = a in Succ(r) = Succ(a) ดังนั้นโดยอุปนัยplus a Zero = aสำหรับทุกคนaในนัท

แน่นอนเราสามารถพิสูจน์สิ่งที่น่าสนใจมากขึ้น แต่นี่เป็นความคิดทั่วไป

จนถึงตอนนี้เราได้จัดการกับข้อมูลที่กำหนดแบบเหนี่ยวนำซึ่งเราได้รับโดยปล่อยให้เป็นชุด "ที่เล็กที่สุด" ดังนั้นตอนนี้เราต้องการที่จะทำงานร่วมกับcodata ที่กำหนดไว้โดย coinductivly ที่เราได้รับโดยให้มันเป็นชุดที่ใหญ่ที่สุด

ดังนั้น

ปล่อยให้เป็นเซต ชุด "สตรีมของ" ถูกกำหนดให้เป็นชุดที่ใหญ่ที่สุดเช่นว่าสำหรับแต่ละ x ในสตรีมของ a, x ประกอบด้วยคู่ที่สั่งซื้อ (หัวหาง) ซึ่งหัวอยู่ในและท้ายอยู่ในสตรีมของ

ใน Haskell เราจะแสดงสิ่งนี้เป็น

data Stream a = Stream a (Stream a) --"data" not "newtype"

ที่จริงแล้วใน Haskell เราใช้รายการในตัวตามปกติซึ่งอาจเป็นคู่ที่สั่งหรือรายการที่ว่างเปล่า

data [a] = [] | a:[a]

Banana ไม่ใช่สมาชิกของประเภทนี้เช่นกันเนื่องจากไม่ใช่คู่ที่สั่งซื้อหรือรายการว่าง แต่ตอนนี้เราสามารถพูดได้

ones = 1:ones

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

ones = 1:ones

เป็นแบบเรียกซ้ำซ้อน ในขณะที่ฟังก์ชั่นmap(เช่น "foreach" ในภาษาบังคับ) เป็นทั้งแบบเรียกซ้ำ (แบบเรียงลำดับ) และแบบเรียกซ้ำแบบดั้งเดิม

map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = (f x):map f xs

ไปสำหรับฟังก์ชั่นzipWithที่ใช้ฟังก์ชั่นและคู่ของรายการและรวมเข้าด้วยกันโดยใช้ฟังก์ชั่นนั้น

zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:as) (b:bs) = (f a b):zipWith f as bs
zipWith _ _ _           = [] --base case

ตัวอย่างคลาสสิกของภาษาที่ใช้งานได้คือลำดับฟีโบนักชี

fib 0 = 0
fib 1 = 1
fib n = (fib (n-1)) + (fib (n-2))

ซึ่งเรียกซ้ำแบบดั้งเดิม แต่สามารถแสดงได้อย่างสง่างามมากขึ้นเป็นรายการที่ไม่มีที่สิ้นสุด

fibs = 0:1:zipWith (+) fibs (tail fibs)
fib' n = fibs !! n --the !! is haskell syntax for index at

ตัวอย่างที่น่าสนใจของการเหนี่ยวนำ / การทำเหรียญคือการพิสูจน์ว่าคำจำกัดความทั้งสองนี้คำนวณสิ่งเดียวกัน นี่เป็นแบบฝึกหัดสำหรับผู้อ่าน


1
@ user1131997 ขอบคุณ ฉันกำลังวางแผนที่จะแปลโค้ดบางส่วนเป็นภาษาจาวาคอยติดตาม
Philip JF

@PhilipJF: ฉันรู้สึกงี่เง่า แต่ฉันไม่เห็นว่าทำไม "... เห็นได้ชัดว่า Zero อยู่ใน N และถ้า y อยู่ใน N Succ (y) อยู่ใน N ... " จะเกิดอะไรขึ้นถ้าคุณพอใจกับ Succ (y) = omega (เพราะคุณไม่ได้ใช้คุณสมบัติของ Zero และ Succ ฉันสามารถแทนที่ Succ = รูตสแควร์และ Zero = 2)
Ta Thanh Dinh

... และจากนั้นฉันเห็น omega = 1
Ta Thanh Dinh

เป้าหมายคือการแสดงโอเมก้าไม่ได้อยู่ในนัท เราทำสิ่งนี้โดยแย้ง หากโอเมก้าอยู่ใน nat มากกว่าชุด N = nat - {omega} จะเป็นไปตามกฎหมาย นั่นเป็นเพราะ nat เป็นไปตามกฎหมาย ถ้า y ใน N, 1 y ไม่ใช่โอเมก้าและ 2 y ใน nat จาก 2 เรารู้ว่า Succ (y) ใน nat และโดย 1 y ไม่ใช่ Omega Succ (y) ไม่ใช่ Omega ดังนั้น Succ (y) ใน N. N จึงรวมศูนย์ด้วย แต่ N ก็เล็กกว่า nat นี่คือความขัดแย้ง ดังนั้น nat ไม่รวมโอเมก้า
Philip JF

นี่เป็นเรื่องโกหกเล็กน้อยเนื่องจาก ocaml มีการเรียกซ้ำคุณค่าฉันควรใช้ SML ซึ่งเป็นภาษาเดียว "หลัก" ที่สนับสนุนการใช้เหตุผลเชิงอุปนัย
Philip JF

10

โดยทั่วไปการสำรวจคือการสะสมแบบเรียกซ้ำการสร้างผลลัพธ์ระหว่างทางจากกรณีเริ่มต้นในขณะที่การเรียกซ้ำปกติจะสร้างผลลัพธ์ในทางกลับจากกรณีพื้นฐาน

(พูด Haskell ตอนนี้) นั่นเป็นเหตุผลที่foldr(ด้วยฟังก์ชั่นการรวมอย่างเข้มงวด) เป็นการแสดงออกถึงการเรียกซ้ำและfoldl'(ด้วยการใช้หวีอย่างเข้มงวด f.) / scanl/ until/ iterate/ unfoldr/ ฯลฯ การสำรวจมีอยู่ทุกที่ foldrด้วยหวีที่ไม่เข้มงวด ฉ เป็นการแสดงออกถึงหาง recursion ข้อเสียแบบโมดูโล

และของ Haskell recursion รักษาเป็นเพียงเหมือนหาง recursion ข้อเสียแบบโมดูโล

นี่คือการเรียกซ้ำ:

fib n | n==0 = 0
      | n==1 = 1
      | n>1  = fib (n-1) + fib (n-2)

fib n = snd $ g n
  where
    g n | n==0 = (1,0)
        | n>0  = let { (b,a) = g (n-1) } in (b+a,b)

fib n = snd $ foldr (\_ (b,a) -> (b+a,b)) (1,0) [n,n-1..1]

(อ่าน$ว่า "จาก") นี่คือการสำรวจ:

fib n = g (0,1) 0 n where
  g n (a,b) i | i==n      = a 
              | otherwise = g n (b,a+b) (i+1)

fib n = fst.snd $ until ((==n).fst) (\(i,(a,b)) -> (i+1,(b,a+b))) (0,(0,1))
      = fst $ foldl (\(a,b) _ -> (b,a+b)) (0,1) [1..n]
      = fst $ last $ scanl (\(a,b) _ -> (b,a+b)) (0,1) [1..n]
      = fst (fibs!!n)  where  fibs = scanl (\(a,b) _ -> (b,a+b)) (0,1) [1..]
      = fst (fibs!!n)  where  fibs = iterate (\(a,b) -> (b,a+b)) (0,1)
      = (fibs!!n)  where  fibs = unfoldr (\(a,b) -> Just (a, (b,a+b))) (0,1)
      = (fibs!!n)  where  fibs = 0:1:map (\(a,b)->a+b) (zip fibs $ tail fibs)
      = (fibs!!n)  where  fibs = 0:1:zipWith (+) fibs (tail fibs)
      = (fibs!!n)  where  fibs = 0:scanl (+) 1 fibs
      = .....

เท่า: http://en.wikipedia.org/wiki/Fold_(higher-order_function)


4

ตรวจสอบเรื่องนี้ที่บล็อก Vitomir Kovanovic' s ฉันพบมันจนถึงจุดนี้:

การประเมิน Lazy ในคุณสมบัติที่ดีอย่างหนึ่งที่พบในภาษาการเขียนโปรแกรมที่มีความสามารถในการเขียนโปรแกรมใช้งานได้เช่น lisp, haskell, python เป็นต้นการประเมินค่าตัวแปรล่าช้าไปถึงการใช้งานจริงของตัวแปรนั้น

มันหมายความว่าตัวอย่างเช่นถ้าคุณต้องการสร้างรายการองค์ประกอบนับล้านด้วยบางสิ่งเช่นนี้(defn x (range 1000000))มันไม่ได้ถูกสร้างขึ้นจริง แต่มีการระบุไว้และเมื่อคุณใช้ตัวแปรนั้นเป็นครั้งแรกเช่นเมื่อคุณต้องการองค์ประกอบที่ 10 ของ list interpreter นั้นสร้างเพียง 10 องค์ประกอบแรกของรายการนั้น ดังนั้นการรันครั้งแรกของ (รับ 10 x) จะสร้างองค์ประกอบเหล่านี้จริง ๆ และการเรียกฟังก์ชั่นที่เหมือนกันต่อมาทั้งหมดจะทำงานกับองค์ประกอบที่มีอยู่แล้ว

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

ในอีกทางหนึ่งการสำรวจเป็นสองเท่าเพื่อเรียกซ้ำ หมายความว่าอะไร? เช่นเดียวกับฟังก์ชั่นวนซ้ำซึ่งแสดงออกมาในแง่ของตัวเองตัวแปรที่เป็นค่า Corecursive จะแสดงออกมาในรูปของตัวเอง

นี่คือตัวอย่างที่ดีที่สุด

สมมติว่าเราต้องการรายการหมายเลขเฉพาะทั้งหมด ...


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