หากภาษาโปรแกรมที่ใช้งานได้ไม่สามารถบันทึกสถานะใด ๆ ได้พวกเขาจะทำสิ่งง่ายๆเช่นการอ่านอินพุตจากผู้ใช้อย่างไร (ฉันหมายถึงว่าพวกเขา "จัดเก็บ" มันอย่างไร) หรือจัดเก็บข้อมูลใด ๆ
ในขณะที่คุณรวบรวมโปรแกรมการทำงานจะไม่มีสถานะ แต่ไม่ได้หมายความว่าจะไม่สามารถจัดเก็บข้อมูลได้ ความแตกต่างคือถ้าฉันเขียนคำสั่ง (Haskell) ตามบรรทัดของ
let x = func value 3.14 20 "random"
in ...
ฉันรับประกันว่าค่าของx
จะเหมือนกันเสมอใน...
: ไม่มีอะไรสามารถเปลี่ยนแปลงได้ ในทำนองเดียวกันถ้าฉันมีฟังก์ชันf :: String -> Integer
(ฟังก์ชันรับสตริงและส่งคืนจำนวนเต็ม) ฉันมั่นใจได้ว่าf
จะไม่แก้ไขอาร์กิวเมนต์หรือเปลี่ยนตัวแปรส่วนกลางใด ๆ หรือเขียนข้อมูลลงในไฟล์และอื่น ๆ ดังที่คุณ sepp2k กล่าวไว้ในความคิดเห็นข้างต้นการไม่เปลี่ยนแปลงนี้มีประโยชน์อย่างมากสำหรับการใช้เหตุผลเกี่ยวกับโปรแกรม: คุณเขียนฟังก์ชันที่พับแกนและทำให้ข้อมูลของคุณเสียหายส่งคืนสำเนาใหม่เพื่อให้คุณสามารถเชื่อมโยงเข้าด้วยกันและคุณมั่นใจได้ว่าไม่มี ของการเรียกใช้ฟังก์ชันเหล่านั้นสามารถทำอะไรก็ได้ที่ "เป็นอันตราย" คุณรู้ว่าx
เป็นเช่นนั้นเสมอx
และคุณไม่ต้องกังวลว่าจะมีใครบางคนเขียนไว้x := foo bar
ที่ไหนสักแห่งระหว่างคำประกาศของx
และการใช้งานเพราะเป็นไปไม่ได้
ตอนนี้ถ้าฉันต้องการอ่านข้อมูลจากผู้ใช้ล่ะ? ดังที่ Kenny ™กล่าวแนวคิดก็คือฟังก์ชันที่ไม่บริสุทธิ์เป็นฟังก์ชันบริสุทธิ์ที่ส่งผ่านโลกทั้งใบเป็นอาร์กิวเมนต์และส่งกลับทั้งผลลัพธ์และโลก แน่นอนว่าคุณไม่ต้องการทำสิ่งนี้จริงๆ: ประการหนึ่งมันน่ากลัวอย่างมากและอีกอย่างจะเกิดอะไรขึ้นถ้าฉันนำวัตถุโลกเดียวกันกลับมาใช้ใหม่ ดังนั้นสิ่งนี้จึงถูกทำให้เป็นนามธรรม Haskell จัดการกับประเภท IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
สิ่งนี้บอกเราว่าmain
เป็นการกระทำของ IO ซึ่งไม่ส่งคืนอะไรเลย การเรียกใช้การกระทำนี้คือความหมายของการรันโปรแกรม Haskell กฎคือประเภท IO ไม่สามารถหลีกเลี่ยงการกระทำของ IO ได้ do
ในบริบทนี้เราแนะนำว่าการใช้การดำเนินการ ดังนั้นgetLine
ผลตอบแทน an IO String
ซึ่งสามารถคิดได้สองวิธี: ประการแรกเป็นการกระทำซึ่งเมื่อเรียกใช้จะสร้างสตริง ประการที่สองเป็นสตริงที่ IO "แปดเปื้อน" เนื่องจากได้รับมาอย่างไม่บริสุทธิ์ อย่างแรกถูกต้องกว่า แต่อย่างที่สองจะมีประโยชน์มากกว่า <-
เตะString
ออกจากIO String
และเก็บไว้ในstr
-but ตั้งแต่เราอยู่ในการดำเนินการ IO เราจะมีการห่อมันกลับขึ้นดังนั้นจึงไม่สามารถ "หลบหนี" บรรทัดถัดไปพยายามอ่านจำนวนเต็ม ( reads
) และจับคู่แรกที่ประสบความสำเร็จ (fst . head
); ทั้งหมดนี้บริสุทธิ์ (ไม่มี IO) ดังนั้นเราจึงตั้งชื่อด้วยlet no = ...
. จากนั้นเราสามารถใช้ทั้งสองอย่างno
และstr
ในไฟล์...
. ดังนั้นเราจึงจัดเก็บข้อมูลที่ไม่บริสุทธิ์ (จากgetLine
เข้าสู่str
) และข้อมูลบริสุทธิ์ ( let no = ...
)
กลไกในการทำงานกับ IO นี้มีประสิทธิภาพมาก: ช่วยให้คุณแยกส่วนอัลกอริทึมที่บริสุทธิ์ของโปรแกรมของคุณออกจากด้านที่ไม่บริสุทธิ์การโต้ตอบกับผู้ใช้และบังคับใช้สิ่งนี้ในระดับประเภท minimumSpanningTree
ฟังก์ชันของคุณไม่สามารถเปลี่ยนแปลงอย่างอื่นในโค้ดของคุณหรือเขียนข้อความถึงผู้ใช้ของคุณและอื่น ๆ ได้ มันปลอดภัย.
นี่คือทั้งหมดที่คุณต้องรู้เพื่อใช้ IO ใน Haskell ถ้านั่นคือทั้งหมดที่คุณต้องการคุณสามารถหยุดที่นี่ แต่ถ้าคุณต้องการเข้าใจว่าเหตุใดจึงได้ผลโปรดอ่านต่อไป (และโปรดทราบว่าสิ่งนี้จะเฉพาะสำหรับ Haskell - ภาษาอื่น ๆ อาจเลือกการใช้งานที่แตกต่างกัน)
ดังนั้นนี่อาจดูเหมือนเป็นการโกงเล็กน้อย แต่ก็เป็นการเพิ่มความไม่บริสุทธิ์ให้กับ Haskell ที่บริสุทธิ์ แต่ไม่ใช่ - ปรากฎว่าเราสามารถใช้งานประเภท IO ได้ทั้งหมดภายใน Haskell แท้ (ตราบเท่าที่เราได้รับRealWorld
) ความคิดนี้คือ: การกระทำ IO IO type
เป็นเช่นเดียวกับฟังก์ชั่นRealWorld -> (type, RealWorld)
ซึ่งจะมีโลกแห่งความจริงและผลตอบแทนทั้งสองประเภทของวัตถุและการแก้ไขtype
RealWorld
จากนั้นเรากำหนดฟังก์ชันสองสามอย่างเพื่อให้เราสามารถใช้ประเภทนี้ได้โดยไม่ต้องเสียสติ:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
คนแรกที่ช่วยให้เราสามารถที่จะพูดคุยเกี่ยวกับการกระทำ IO ซึ่งไม่ได้ทำอะไร: return 3
คือการกระทำ IO 3
ซึ่งไม่ได้สอบถามโลกแห่งความจริงและเพียงแค่ผลตอบแทน >>=
ประกอบการออกเสียง "ผูก" ช่วยให้เราสามารถเรียกใช้การกระทำ IO มันแยกค่าจากการดำเนินการ IO ส่งผ่านมันและโลกแห่งความเป็นจริงผ่านฟังก์ชันและส่งคืนการกระทำ IO ที่เป็นผลลัพธ์ โปรดทราบว่า>>=
บังคับใช้กฎของเราว่าผลของการกระทำ IO จะไม่ได้รับอนุญาตให้หลบหนี
จากนั้นเราสามารถเปลี่ยนข้างต้นmain
ให้เป็นชุดแอพพลิเคชั่นฟังก์ชั่นปกติดังต่อไปนี้:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
รันไทม์ Haskell เริ่มต้นmain
ด้วยการเริ่มต้นRealWorld
และเราพร้อมแล้ว! ทุกอย่างบริสุทธิ์มีเพียงไวยากรณ์ที่สวยงาม
[ แก้ไข: ตามที่ @Conal ชี้ให้เห็นว่านี่ไม่ใช่สิ่งที่ Haskell ใช้ทำ IO โมเดลนี้จะหยุดทำงานหากคุณเพิ่มการทำงานพร้อมกันหรือวิธีใด ๆ ที่ทำให้โลกเปลี่ยนไปในช่วงกลางของการดำเนินการ IO ดังนั้นจึงเป็นไปไม่ได้ที่ Haskell จะใช้โมเดลนี้ มีความแม่นยำสำหรับการคำนวณตามลำดับเท่านั้น ดังนั้นจึงอาจเป็นไปได้ว่า IO ของ Haskell ค่อนข้างหลบ แม้ว่าจะไม่ใช่ แต่ก็ไม่ได้สวยหรูขนาดนี้ ข้อสังเกตของ Per @ Conal ดูสิ่งที่ Simon Peyton-Jones พูดในTackling the Awesome Squad [pdf]ตอนที่ 3.1; เขานำเสนอสิ่งที่อาจมีค่าต่อโมเดลทางเลือกตามแนวเหล่านี้ แต่จากนั้นก็ลดลงตามความซับซ้อนและใช้วิธีที่แตกต่างกัน
อีกครั้งสิ่งนี้อธิบาย (ค่อนข้างมาก) ว่า IO และความไม่แน่นอนโดยทั่วไปทำงานอย่างไรใน Haskell หากนี่คือทั้งหมดที่คุณต้องการทราบคุณสามารถหยุดอ่านได้ที่นี่ หากคุณต้องการทฤษฎีสุดท้ายอ่านต่อไป แต่จำไว้ว่า ณ จุดนี้เราห่างไกลจากคำถามของคุณมาก!
สิ่งสุดท้าย: ปรากฎว่าโครงสร้างนี้ - ประเภทพาราเมตริกที่มีreturn
และ>>=
- เป็นเรื่องทั่วไปมาก มันเรียกว่า monad และdo
สัญกรณ์return
และ>>=
ทำงานร่วมกับสิ่งเหล่านี้ อย่างที่คุณเห็นที่นี่ monads ไม่ได้มีมนต์ขลัง สิ่งที่มหัศจรรย์คือdo
บล็อกที่เปลี่ยนเป็นการเรียกฟังก์ชัน RealWorld
ประเภทเป็นสถานที่เดียวที่เราเห็นความมหัศจรรย์ใด ๆ ประเภทเช่น[]
ตัวสร้างรายการเป็น monads และไม่มีส่วนเกี่ยวข้องกับรหัสที่ไม่บริสุทธิ์
ตอนนี้คุณรู้ (เกือบ) ทุกอย่างเกี่ยวกับแนวคิดของ monad แล้ว (ยกเว้นกฎหมายบางอย่างที่ต้องเป็นที่พอใจและคำจำกัดความทางคณิตศาสตร์ที่เป็นทางการ) แต่คุณขาดสัญชาตญาณ มีบทเรียนออนไลน์ที่ไร้สาระมากมาย ฉันชอบอันนี้แต่คุณมีตัวเลือก อย่างไรก็ตามเรื่องนี้อาจจะไม่ได้ช่วยให้คุณ ; วิธีเดียวที่จะได้รับสัญชาตญาณคือการใช้พวกมันร่วมกันและอ่านบทช่วยสอนสองสามบทในเวลาที่เหมาะสม
แต่คุณไม่จำเป็นต้องสัญชาตญาณที่จะเข้าใจ IO การทำความเข้าใจกับ monads โดยทั่วไปคือไอซิ่งบนเค้ก แต่คุณสามารถใช้ IO ได้ในขณะนี้ คุณสามารถใช้ได้หลังจากที่ฉันแสดงmain
ฟังก์ชันแรก คุณสามารถปฏิบัติต่อรหัส IO ราวกับว่ามันเป็นภาษาที่ไม่บริสุทธิ์! แต่จำไว้ว่ามีการแสดงหน้าที่อยู่: ไม่มีใครโกง
(ป.ล. : ขออภัยเกี่ยวกับความยาวฉันไปไกลเล็กน้อย)