ความแตกต่างระหว่างสิ่งเหล่านี้คืออะไร?
บนวิกิพีเดียมีข้อมูลเพียงเล็กน้อยและไม่มีรหัสที่ชัดเจนที่อธิบายถึงข้อกำหนดเหล่านี้
อะไรคือตัวอย่างง่ายๆที่อธิบายคำศัพท์เหล่านี้
การกลับเป็นสองเท่าของการเรียกซ้ำเป็นอย่างไร?
มีอัลกอริธึมการยืนยันคลาสสิกหรือไม่?
ความแตกต่างระหว่างสิ่งเหล่านี้คืออะไร?
บนวิกิพีเดียมีข้อมูลเพียงเล็กน้อยและไม่มีรหัสที่ชัดเจนที่อธิบายถึงข้อกำหนดเหล่านี้
อะไรคือตัวอย่างง่ายๆที่อธิบายคำศัพท์เหล่านี้
การกลับเป็นสองเท่าของการเรียกซ้ำเป็นอย่างไร?
มีอัลกอริธึมการยืนยันคลาสสิกหรือไม่?
คำตอบ:
มีหลายวิธีที่ดีในการดูที่นี้ สิ่งที่ง่ายที่สุดสำหรับฉันคือคิดถึงความสัมพันธ์ระหว่าง "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
ตัวอย่างที่น่าสนใจของการเหนี่ยวนำ / การทำเหรียญคือการพิสูจน์ว่าคำจำกัดความทั้งสองนี้คำนวณสิ่งเดียวกัน นี่เป็นแบบฝึกหัดสำหรับผู้อ่าน
โดยทั่วไปการสำรวจคือการสะสมแบบเรียกซ้ำการสร้างผลลัพธ์ระหว่างทางจากกรณีเริ่มต้นในขณะที่การเรียกซ้ำปกติจะสร้างผลลัพธ์ในทางกลับจากกรณีพื้นฐาน
(พูด 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)
ตรวจสอบเรื่องนี้ที่บล็อก Vitomir Kovanovic' s ฉันพบมันจนถึงจุดนี้:
การประเมิน Lazy ในคุณสมบัติที่ดีอย่างหนึ่งที่พบในภาษาการเขียนโปรแกรมที่มีความสามารถในการเขียนโปรแกรมใช้งานได้เช่น lisp, haskell, python เป็นต้นการประเมินค่าตัวแปรล่าช้าไปถึงการใช้งานจริงของตัวแปรนั้น
มันหมายความว่าตัวอย่างเช่นถ้าคุณต้องการสร้างรายการองค์ประกอบนับล้านด้วยบางสิ่งเช่นนี้
(defn x (range 1000000))มันไม่ได้ถูกสร้างขึ้นจริง แต่มีการระบุไว้และเมื่อคุณใช้ตัวแปรนั้นเป็นครั้งแรกเช่นเมื่อคุณต้องการองค์ประกอบที่ 10 ของ list interpreter นั้นสร้างเพียง 10 องค์ประกอบแรกของรายการนั้น ดังนั้นการรันครั้งแรกของ (รับ 10 x) จะสร้างองค์ประกอบเหล่านี้จริง ๆ และการเรียกฟังก์ชั่นที่เหมือนกันต่อมาทั้งหมดจะทำงานกับองค์ประกอบที่มีอยู่แล้วสิ่งนี้มีประโยชน์มากเพราะคุณสามารถสร้างรายการที่ไม่มีที่สิ้นสุดโดยไม่มีข้อผิดพลาดของหน่วยความจำรายการจะมีขนาดใหญ่เท่าที่คุณร้องขอ แน่นอนว่าหากโปรแกรมของคุณทำงานกับการรวบรวมข้อมูลขนาดใหญ่โปรแกรมอาจถึงขีด จำกัด หน่วยความจำในการใช้งานรายการที่ไม่มีที่สิ้นสุดเหล่านี้
ในอีกทางหนึ่งการสำรวจเป็นสองเท่าเพื่อเรียกซ้ำ หมายความว่าอะไร? เช่นเดียวกับฟังก์ชั่นวนซ้ำซึ่งแสดงออกมาในแง่ของตัวเองตัวแปรที่เป็นค่า Corecursive จะแสดงออกมาในรูปของตัวเอง
นี่คือตัวอย่างที่ดีที่สุด
สมมติว่าเราต้องการรายการหมายเลขเฉพาะทั้งหมด ...