ฉันจะใช้การแก้ไขได้อย่างไรและทำงานอย่างไร


90

ฉันสับสนเล็กน้อยกับเอกสารสำหรับfix(แม้ว่าฉันคิดว่าฉันเข้าใจว่ามันควรจะทำอะไรในตอนนี้) ดังนั้นฉันจึงดูซอร์สโค้ด นั่นทำให้ฉันสับสนมากขึ้น:

fix :: (a -> a) -> a
fix f = let x = f x in x

สิ่งนี้ส่งคืนจุดคงที่ได้อย่างไร?

ฉันตัดสินใจลองใช้ที่บรรทัดคำสั่ง:

Prelude Data.Function> fix id
...

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


69
คำตอบที่เล่นพิเรนคือ "การแก้ไขไม่มีประโยชน์จริงๆมีเพียงแค่นั้นเพื่อให้คุณพิมพ์fix errorghci และรู้สึกดีกับตัวเอง"
Thomas M. DuBuisson

3
@TomMD - ตลกดี ฉันจะจำไว้ว่าถ้าใครเคยถามฉันว่าแก้ไขอะไรได้บ้างและฉันรู้สึกหรูหรา :-)
Jason Baker

2
ฉันมักจะเขียนfixเป็นfix f = f (fix f). สั้นง่ายใช้งานได้และเหมือนกับนิยามทางคณิตศาสตร์
newacct

20
@newacct ใช่แล้วฉันก็คิดเหมือนกัน แต่สิ่งนี้สามารถนำไปสู่โครงสร้างที่มีประสิทธิภาพมากขึ้น fix (1:) !! (10^8)คุณสามารถเห็นความแตกต่างถ้าคุณประเมินการพูด ต้นฉบับทำในหน่วยความจำคงที่ของคุณใช้หน่วยความจำเชิงเส้น (ซึ่งทำให้ช้าลงเล็กน้อยด้วย) นั่นคือใช้การปล่อยให้ "ผูกปมที่แน่นกว่า" และอนุญาตให้สร้างโครงสร้างข้อมูลแบบวงกลมในขณะที่ของคุณไม่ทำ
luqui

คำตอบ:


92

คุณไม่ได้ทำอะไรผิด fix idเป็นวงวนที่ไม่มีที่สิ้นสุด

เมื่อเราบอกว่าfixส่งคืนจุดคงที่น้อยที่สุดของฟังก์ชันเราหมายความว่าในแง่ทฤษฎีโดเมน ดังนั้นfix (\x -> 2*x-1)จะไม่กลับมา1เพราะแม้ว่า1จะเป็นจุดคงที่ของฟังก์ชันนั้น แต่ก็ไม่ใช่จุดที่น้อยที่สุดในการเรียงลำดับโดเมน

ฉันไม่สามารถอธิบายการเรียงลำดับโดเมนในย่อหน้าหรือสองย่อหน้าได้ดังนั้นฉันจะแนะนำคุณไปยังลิงก์ทฤษฎีโดเมนด้านบน เป็นแบบฝึกหัดที่ยอดเยี่ยมอ่านง่ายและให้ความกระจ่าง ฉันขอแนะนำอย่างยิ่ง

สำหรับมุมมองจาก 10,000 ฟุตที่fixเป็นฟังก์ชั่นขั้นสูงซึ่งถอดรหัสความคิดของการเรียกซ้ำ หากคุณมีนิพจน์:

let x = 1:x in x

ซึ่งผลลัพธ์ในรายการไม่มีที่สิ้นสุด[1,1..]คุณสามารถพูดในสิ่งเดียวกันโดยใช้fix:

fix (\x -> 1:x)

(หรือง่ายๆfix (1:)) ซึ่งบอกว่าหาจุดคงที่ของ(1:)ฟังก์ชัน IOW เป็นค่าxเช่นนั้นx = 1:x... เหมือนที่เรากำหนดไว้ข้างต้น ดังที่คุณเห็นจากคำจำกัดความfixไม่มีอะไรมากไปกว่าแนวคิดนี้ - การเรียกซ้ำที่ห่อหุ้มไว้ในฟังก์ชัน

มันเป็นแนวคิดทั่วไปอย่างแท้จริงของการเรียกซ้ำเช่นกัน - คุณสามารถเขียนฟังก์ชันเวียนใด ๆ วิธีนี้รวมถึงฟังก์ชั่นที่ใช้เรียกซ้ำ polymorphic ตัวอย่างเช่นฟังก์ชัน fibonacci ทั่วไป:

fib n = if n < 2 then n else fib (n-1) + fib (n-2)

สามารถเขียนโดยใช้fixวิธีนี้:

fib = fix (\f -> \n -> if n < 2 then n else f (n-1) + f (n-2))

แบบฝึกหัด: ขยายคำจำกัดความของfixเพื่อแสดงว่าคำจำกัดความทั้งสองfibนี้เทียบเท่ากัน

แต่เพื่อความเข้าใจอย่างถ่องแท้โปรดอ่านเกี่ยวกับทฤษฎีโดเมน มันเจ๋งมาก


32
นี่คือวิธีที่เกี่ยวข้องกับการคิดเกี่ยวกับfix id: fixจะใช้เวลาการทำงานของชนิดและผลตอบแทนค่าของชนิดa -> a aเพราะidเป็น polymorphic สำหรับการใด ๆa, fix idจะมีชนิดaเช่นค่าที่เป็นไปได้ใด ๆ ใน Haskell ค่าเดียวที่สามารถเป็นประเภทใดก็ได้คือด้านล่าง⊥และแยกไม่ออกจากการคำนวณแบบไม่สิ้นสุด ดังนั้นfix idให้สร้างสิ่งที่ควรจะเป็นค่าด้านล่าง อันตรายfixก็คือถ้า⊥เป็นจุดคงที่ของฟังก์ชันของคุณมันก็คือจุดคงที่น้อยที่สุดตามนิยามดังนั้นfixจะไม่ยุติ
John L

4
@JohnL ใน Haskell undefinedเป็นค่าประเภทใดก็ได้ คุณสามารถกำหนดfixเป็น: fix f = foldr (\_ -> f) undefined (repeat undefined).
didest

1
@Diego รหัสของคุณเทียบเท่ากับ_Y f = f (_Y f).
Will Ness

27

ฉันไม่ได้อ้างว่าเข้าใจสิ่งนี้เลย แต่ถ้าสิ่งนี้ช่วยใคร ...

พิจารณาคำจำกัดความของfix. fix f = let x = f x in x. ส่วนที่เหลือเชื่อคือการที่ถูกกำหนดให้เป็นx f xแต่ลองคิดดูสักนาที

x = f x

เนื่องจาก x = fx เราจึงแทนค่าxทางขวามือของมันได้ใช่ไหม? ดังนั้น ...

x = f . f $ x -- or x = f (f x)
x = f . f . f $ x -- or x = f (f (f x))
x = f . f . f . f . f . f . f . f . f . f . f $ x -- etc.

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

เว้นแต่คุณต้องการทำบางสิ่งบางอย่างเช่นสร้างรายการที่ไม่มีที่สิ้นสุดดังที่ luqui แสดงไว้

คำอธิบายแฟคทอเรียลของ TomMD นั้นดี (a -> a) -> aลายเซ็นประเภทของการแก้ไขปัญหาคือ ลายเซ็นประเภทสำหรับ(\recurse d -> if d > 0 then d * (recurse (d-1)) else 1)คือ(b -> b) -> b -> bกล่าวอีกนัยหนึ่งคือ(b -> b) -> (b -> b). a = (b -> b)ดังนั้นเราจึงสามารถพูดได้ว่า ด้วยวิธีนี้การแก้ไขจะรับฟังก์ชั่นของเราซึ่งก็คือa -> aหรือจริงๆ(b -> b) -> (b -> b)และจะส่งคืนผลลัพธ์ของประเภทaกล่าวb -> bอีกนัยหนึ่งคือฟังก์ชันอื่น!

เดี๋ยวก่อนฉันคิดว่ามันควรจะคืนค่าจุดคงที่ ... ไม่ใช่ฟังก์ชัน มันก็เป็นเช่นนั้น (เนื่องจากฟังก์ชันเป็นข้อมูล) คุณคงนึกออกว่ามันทำให้เรามีฟังก์ชันสรุปในการหาแฟกทอเรียล เราให้ฟังก์ชันที่ไม่รู้ว่าจะเรียกคืนได้อย่างไร (ดังนั้นหนึ่งในพารามิเตอร์ของมันคือฟังก์ชันที่ใช้ในการเรียกคืน) และfixสอนวิธีการเรียกคืน

จำได้ไหมว่าฉันบอกว่าfต้องสร้างโครงสร้างบางอย่างเพื่อให้fรูปแบบในภายหลังสามารถจับคู่และยุติได้? นั่นไม่ถูกต้องฉันเดา TomMD แสดงให้เห็นว่าเราสามารถขยายxเพื่อใช้ฟังก์ชันและก้าวไปสู่กรณีพื้นฐานได้อย่างไร สำหรับฟังก์ชันของเขาเขาใช้ if / then และนั่นคือสิ่งที่ทำให้เกิดการยกเลิก หลังจากการเปลี่ยนซ้ำแล้วซ้ำอีกinส่วนของคำจำกัดความทั้งหมดของคำจำกัดความทั้งหมดของการfixหยุดในที่สุดจะถูกกำหนดในแง่ของxและนั่นคือเมื่อคำนวณได้และสมบูรณ์


ขอบคุณ. นี่เป็นคำอธิบายที่มีประโยชน์และใช้ได้จริง
kizzx2

18

คุณต้องมีวิธีเพื่อให้ fixpoint ยุติ การขยายตัวอย่างของคุณเห็นได้ชัดว่ามันจะไม่เสร็จสิ้น:

fix id
--> let x = id x in x
--> id x
--> id (id x)
--> id (id (id x))
--> ...

นี่คือตัวอย่างจริงของฉันที่ใช้การแก้ไข (โปรดทราบว่าฉันไม่ได้ใช้การแก้ไขบ่อยครั้งและอาจเหนื่อย / ไม่ต้องกังวลกับรหัสที่อ่านได้เมื่อฉันเขียนสิ่งนี้)

(fix (\f h -> if (pred h) then f (mutate h) else h)) q

WTF คุณพูด! ใช่แล้ว แต่มีบางประเด็นที่เป็นประโยชน์จริงๆที่นี่ ก่อนอื่นfixอาร์กิวเมนต์แรกของคุณควรเป็นฟังก์ชันซึ่งเป็นกรณี 'เรียกคืน' และอาร์กิวเมนต์ที่สองคือข้อมูลที่จะดำเนินการ นี่คือรหัสเดียวกับฟังก์ชันที่ตั้งชื่อ:

getQ h
      | pred h = getQ (mutate h)
      | otherwise = h

หากคุณยังสับสนบางทีแฟกทอเรียลอาจเป็นตัวอย่างที่ง่ายกว่า:

fix (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) 5 -->* 120

สังเกตการประเมิน:

fix (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) 3 -->
let x = (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x in x 3 -->
let x = ... in (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x 3 -->
let x = ... in (\d -> if d > 0 then d * (x (d-1)) else 1) 3

โอ้คุณเพิ่งเห็นเหรอ? นั่นxกลายเป็นหน้าที่ในthenสาขา ของเรา

let x = ... in if 3 > 0 then 3 * (x (3 - 1)) else 1) -->
let x = ... in 3 * x 2 -->
let x = ... in 3 * (\recurse d -> if d > 0 then d * (recurse (d-1)) else 1) x 2 -->

ในข้างต้นคุณต้องจำไว้x = f xดังนั้นทั้งสองขัดแย้งของที่สิ้นสุดแทนเพียงx 22

let x = ... in 3 * (\d -> if d > 0 then d * (x (d-1)) else 1) 2 -->

และฉันจะหยุดที่นี่!


คำตอบของคุณคือสิ่งที่fixสมเหตุสมผลสำหรับฉัน คำตอบของฉันส่วนใหญ่ขึ้นอยู่กับสิ่งที่คุณได้พูดไปแล้ว
Dan Burton

@ โทมัสลำดับการลดของคุณทั้งสองไม่ถูกต้อง :) id xเพียงแค่ลดเป็นx(ซึ่งจะลดกลับเป็นid x) - จากนั้นในตัวอย่างที่ 2 ( fact) เมื่อxthunk ถูกบังคับครั้งแรกค่าผลลัพธ์จะถูกจดจำและนำกลับมาใช้ การคำนวณใหม่ของ(\recurse ...) xจะเกิดขึ้นกับคำจำกัดความที่ไม่ใช่การแบ่งปันy g = g (y g)ไม่ใช่กับคำจำกัดความการแบ่งปัน fix- ฉันได้ทำการทดลองแก้ไขแล้วที่นี่ - คุณสามารถใช้มันได้หรือฉันสามารถแก้ไขได้หากคุณอนุมัติ
Will Ness

ที่จริงแล้วเมื่อfix idลดลงlet x = id x in xก็บังคับให้ค่าของแอปพลิเคชันid xภายในletเฟรม (thunk) ลดลงเป็นlet x = x in xและลูปนี้ ดูเหมือนมัน.
Will Ness

แก้ไข. คำตอบของฉันคือการใช้เหตุผลเชิงสมการ การแสดงการลด a la Haskell ซึ่งเกี่ยวข้องกับคำสั่งการประเมินผลเพียงทำหน้าที่ทำให้ตัวอย่างสับสนโดยไม่มีผลประโยชน์ที่แท้จริง
Thomas M. DuBuisson

1
คำถามจะถูกแท็กด้วยทั้ง haskell และ letrec (เช่นการให้เรียกซ้ำพร้อมการแชร์) ความแตกต่างระหว่างfixและYนั้นชัดเจนและสำคัญมากใน Haskell ฉันไม่เห็นว่ามีอะไรดีบ้างโดยแสดงลำดับการลดที่ไม่ถูกต้องเมื่อคำสั่งที่ถูกต้องนั้นสั้นกว่าชัดเจนกว่าและติดตามง่ายกว่ามากและสะท้อนให้เห็นถึงสิ่งที่เกิดขึ้นจริงอย่างถูกต้อง
Will Ness

3

ฉันเข้าใจว่ามันเป็นอย่างไรมันพบค่าของฟังก์ชันดังนั้นจึงให้ผลลัพธ์เหมือนกับที่คุณให้ไว้ สิ่งที่จับได้คือมันจะเลือก undefined เสมอ (หรือลูปไม่มีที่สิ้นสุดใน haskell, undefined และ infinite loop จะเหมือนกัน) หรืออะไรก็ตามที่มี undefineds มากที่สุดในนั้น ตัวอย่างเช่นด้วย id

λ <*Main Data.Function>: id undefined
*** Exception: Prelude.undefined

อย่างที่คุณเห็น undefined เป็นจุดคงที่ดังนั้นfixจะเลือกสิ่งนั้น หากคุณทำ (\ x-> 1: x) แทน

λ <*Main Data.Function>: undefined
*** Exception: Prelude.undefined
λ <*Main Data.Function>: (\x->1:x) undefined
[1*** Exception: Prelude.undefined

ดังนั้นfixไม่สามารถเลือกไม่ได้กำหนด เพื่อให้เชื่อมต่อกับลูปที่ไม่มีที่สิ้นสุดมากขึ้น

λ <*Main Data.Function>: let y=y in y
^CInterrupted.
λ <*Main Data.Function>: (\x->1:x) (let y=y in y)
[1^CInterrupted.

อีกครั้งความแตกต่างเล็กน้อย แล้วจุดคงที่คืออะไร? repeat 1ให้เราพยายาม

λ <*Main Data.Function>: repeat 1
[1,1,1,1,1,1, and so on
λ <*Main Data.Function>: (\x->1:x) $ repeat 1
[1,1,1,1,1,1, and so on

มันเหมือนกัน! เนื่องจากนี่เป็นจุดคงที่เพียงจุดเดียวfixจึงต้องชำระให้ได้ ขออภัยfixไม่มีการวนซ้ำที่ไม่สิ้นสุดหรือไม่ได้กำหนดไว้สำหรับคุณ

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