ประโยชน์ของรูปแบบ IO monad สำหรับการจัดการผลข้างเคียงทางวิชาการล้วนๆหรือไม่?


17

ขออภัยสำหรับคำถามข้างเคียงอีกคำถามของ FP + แต่ฉันไม่พบคำถามที่มีอยู่ซึ่งตอบคำถามนี้ให้ฉันได้

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

ฉันยังรวบรวมวิธีการของ Haskell เพื่อทำสิ่งนี้ IO monad ประสบความสำเร็จในการทำสิ่งนี้ด้วยการห่อตัวการกระทำแบบ stateful ไว้ในคอนเทนเนอร์เพื่อการดำเนินการในภายหลังโดยพิจารณานอกขอบเขตของโปรแกรม

ฉันพยายามที่จะเข้าใจรูปแบบนี้ แต่จริงๆแล้วเพื่อตรวจสอบว่าจะใช้ในโครงการ Python หรือไม่ดังนั้นต้องการหลีกเลี่ยง Haskell ถ้ามี

ตัวอย่างที่เข้ามาหยาบ

หากโปรแกรมของฉันแปลงไฟล์ XML เป็นไฟล์ JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

ไม่ใช่วิธีการของ IO monad ในการทำสิ่งนี้อย่างมีประสิทธิภาพ:

steps = list(
    read_file,
    convert,
    write_file,
)

ถ้าอย่างนั้นไม่ต้องเรียกขั้นตอนเหล่านั้นให้พ้นจากความรับผิดชอบแต่การให้ล่ามทำเช่นนั้น?

หรือกล่าวอีกนัยหนึ่งก็เหมือนการเขียน:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

จากนั้นคาดหวังให้คนอื่นโทรมาinner()และบอกว่างานของคุณเสร็จสิ้นเพราะmain()มีความบริสุทธิ์

โปรแกรมทั้งหมดจะจบลงใน IO monad โดยพื้นฐานแล้ว

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

ฉันซาบซึ้งในประโยชน์ของการลดและแยกพฤติกรรมไร้รัฐซึ่งอันที่จริงแล้วทำไมฉันจึงสร้างเวอร์ชันที่มีความจำเป็นเช่นนั้น: รวบรวมอินพุตทำสิ่งที่บริสุทธิ์พ่นเอาท์พุทออกมา หวังว่าconvert()จะบริสุทธิ์อย่างสมบูรณ์และได้รับผลประโยชน์จากความสามารถในการปรับตัวเธรดความปลอดภัย ฯลฯ

ฉันยังซาบซึ้งว่าประเภทของ monadic นั้นมีประโยชน์โดยเฉพาะในท่อที่ใช้กับประเภทที่เทียบเคียงกันได้ แต่ไม่เห็นว่าทำไม IO จึงควรใช้ monads ยกเว้นในท่อ

มีประโยชน์เพิ่มเติมในการจัดการกับผลข้างเคียงที่รูปแบบ IO monad นำมาซึ่งฉันพลาดไปหรือไม่?


1
คุณควรดูวิดีโอนี้ ในที่สุดสิ่งมหัศจรรย์ของพระจะถูกเปิดเผยโดยไม่ต้องหันไปใช้ทฤษฎีหมวดหมู่หรือแฮสเค็ลล์ ปรากฎว่าพระสงฆ์มีการแสดงออกเล็กน้อยใน JavaScript และเป็นหนึ่งในเครื่องมือสำคัญของ Ajax พระเป็นที่น่าอัศจรรย์ พวกมันเป็นสิ่งที่เรียบง่ายเกือบจะถูกนำมาใช้อย่างไม่น่าเชื่อด้วยพลังมหาศาลในการจัดการความซับซ้อน แต่การเข้าใจพวกเขาเป็นเรื่องยากอย่างน่าประหลาดใจและคนส่วนใหญ่เมื่อพวกเขามีช่วงเวลา ah-ha ดูเหมือนจะสูญเสียความสามารถในการอธิบายพวกเขาให้ผู้อื่น
Robert Harvey

วิดีโอที่ดีขอบคุณ จริง ๆ แล้วฉันได้เรียนรู้เกี่ยวกับสิ่งนี้จากบทนำของ JS ไปจนถึงการเขียนโปรแกรมเชิงปฏิบัติ แม้ว่าจะได้ดูแล้ว แต่ฉันค่อนข้างแน่ใจว่าคำถามของฉันเฉพาะกับ IO monad ซึ่ง Crock ไม่ครอบคลุมในวิดีโอนั้น
Stu Cox

อืม ... AJAX ไม่ได้พิจารณารูปแบบของ I / O หรือไม่?
Robert Harvey

1
โปรดทราบว่าประเภทของmainโปรแกรม Haskell คือIO ()- การกระทำของ IO นี่ไม่ใช่ฟังก์ชั่น แต่อย่างใด มันเป็นความคุ้มค่า โปรแกรมทั้งหมดของคุณเป็นค่าที่แท้จริงที่มีคำแนะนำที่บอกให้รันไทม์ภาษาสิ่งที่ควรทำ สิ่งที่ไม่บริสุทธิ์ทั้งหมด (จริงๆแล้วเป็นการกระทำของการกระทำ IO) อยู่นอกขอบเขตของโปรแกรมของคุณ
Wyzard

ในตัวอย่างของคุณส่วน monadic คือเมื่อคุณรับผลลัพธ์จากการคำนวณหนึ่งครั้ง ( read_file) และใช้เป็นอาร์กิวเมนต์ให้กับส่วนถัดไป ( write_file) หากคุณมีลำดับของการกระทำที่เป็นอิสระคุณไม่จำเป็นต้องมี Monad
lortabac

คำตอบ:


14

โปรแกรมทั้งหมดจะจบลงใน IO monad โดยพื้นฐานแล้ว

นั่นเป็นสิ่งที่ฉันคิดว่าคุณไม่ได้เห็นจากมุมมองของ Haskellers ดังนั้นเราจึงมีโปรแกรมดังนี้:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

ฉันคิดว่าสิ่งที่แฮสเคลเลอร์เป็นแบบนี้จะเป็นconvertส่วนที่บริสุทธิ์:

  1. อาจเป็นส่วนใหญ่ของโปรแกรมนี้และโดยไกลซับซ้อนกว่าIOส่วน;
  2. สามารถให้เหตุผลและทดสอบโดยไม่ต้องทำIOอะไรเลย

ดังนั้นพวกเขาจึงไม่เห็นว่านี่เป็นconvertเป็น "มี" ในIOแต่ในขณะที่มันถูกแยกออกIOจาก จากประเภทของสิ่งที่convertไม่สามารถพึ่งพาสิ่งที่เกิดขึ้นในการIOกระทำ

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

ฉันจะบอกว่าสิ่งนี้แยกออกเป็นสองสิ่ง:

  1. เมื่อโปรแกรมรันค่าของอาร์กิวเมนต์จะconvertขึ้นอยู่กับสถานะของไฟล์
  2. แต่สิ่งที่convertฟังก์ชั่นทำนั้นไม่ได้ขึ้นอยู่กับสถานะของไฟล์ convertเป็นฟังก์ชั่นเดียวกันเสมอแม้ว่ามันจะถูกเรียกด้วยอาร์กิวเมนต์ที่ต่างกันที่จุดที่ต่างกัน

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

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


ฉันคิดว่านี่เป็นคำอธิบายที่ดีที่สุด (แม้ว่าคนอื่น ๆ จะช่วยอธิบายสิ่งต่าง ๆ ให้ฉันด้วย) ขอบคุณ
Stu Cox

มันคุ้มค่าหรือไม่ที่การใช้ monads ใน python อาจมีประโยชน์น้อยกว่าเนื่องจาก python มีประเภท (static) เพียงประเภทเดียวดังนั้นจึงไม่รับประกันอะไรเลย?
jk

7

มันยากที่จะแน่ใจว่าสิ่งที่คุณหมายถึงโดย "วิชาการล้วน" แต่ฉันคิดว่าคำตอบส่วนใหญ่คือ "ไม่"

ตามที่อธิบายไว้ในการแก้ปัญหาทีมที่น่าอึดอัดใจโดย Simon Peyton Jones ( แนะนำอย่างยิ่งให้อ่าน!) monadic I / O หมายถึงการแก้ปัญหาจริงด้วยวิธีที่ Haskell ใช้จัดการ I / O อ่านตัวอย่างของเซิร์ฟเวอร์ที่มีคำขอและคำตอบซึ่งฉันจะไม่คัดลอกที่นี่ มันเป็นคำแนะนำที่ดีมาก

Haskell ซึ่งแตกต่างจาก Python ให้การสนับสนุนการคำนวณแบบ "บริสุทธิ์" ซึ่งสามารถบังคับใช้โดยระบบประเภทของมัน แน่นอนคุณสามารถใช้วินัยในตนเองเมื่อเขียนโปรแกรมใน Python เพื่อให้สอดคล้องกับสไตล์นี้ แต่สิ่งที่เกี่ยวกับโมดูลที่คุณไม่ได้เขียน? หากไม่มีความช่วยเหลือจากระบบพิมพ์ (และไลบรารีทั่วไป) อาจทำให้ I / O monadic มีประโยชน์น้อยกว่าใน Python ปรัชญาของภาษาไม่ได้หมายถึงการบังคับให้แยก / บริสุทธิ์ไม่บริสุทธิ์อย่างเคร่งครัด

โปรดทราบว่าสิ่งนี้พูดเกี่ยวกับปรัชญาที่แตกต่างกันของ Haskell และ Python มากกว่าเกี่ยวกับว่า I / O เชิงวิชาการเป็นอย่างไร ฉันจะไม่ใช้มันกับ Python

อีกสิ่งหนึ่ง คุณพูด:

โปรแกรมทั้งหมดจะจบลงใน IO monad โดยพื้นฐานแล้ว

มันเป็นความจริงที่mainฟังก์ชั่นHaskell "ใช้ชีวิต" IOแต่โปรแกรม Haskell จริงนั้นไม่ควรใช้IOเมื่อใดก็ตามที่ไม่จำเป็น เกือบทุกฟังก์ชั่นที่คุณเขียนว่าไม่จำเป็นต้องทำ I / O IOไม่ควรมีประเภท

ดังนั้นฉันจะบอกว่าในตัวอย่างสุดท้ายของคุณคุณได้มันย้อนกลับ: mainไม่บริสุทธิ์ (เพราะมันอ่านและเขียนไฟล์) แต่ฟังก์ชั่นหลักเช่นconvertนั้นบริสุทธิ์


3

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

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

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

มีวิธีอื่น ๆ ในการแก้ปัญหาการพึ่งพาคำสั่งซื้อ แต่การใช้ IO monad อาจเป็นวิธีที่ง่ายที่สุด (จากมุมมองของภาษาอย่างน้อย) เพื่อให้ทุกอย่างอยู่ในขอบเขตการทำงานที่ขี้เกียจโดยไม่มีโค้ดจำเป็นเล็กน้อย นอกจากนี้ยังยืดหยุ่นที่สุด ตัวอย่างเช่นคุณสามารถสร้างไปป์ไลน์ IO แบบไดนามิกที่รันไทม์ได้อย่างง่ายดายโดยอิงจากอินพุตของผู้ใช้


2

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

นั่นไม่ใช่แค่การเขียนโปรแกรมใช้งานได้ นั่นเป็นความคิดที่ดีในทุกภาษา ถ้าคุณทำเช่นการทดสอบหน่วยวิธีการที่คุณแยกออกจากกันread_file(), convert()และwrite_file()มาได้อย่างสมบูรณ์แบบตามธรรมชาติเพราะแม้จะconvert()เป็นไกลโดยส่วนใหญ่ที่ซับซ้อนและใหญ่ที่สุดของรหัสการเขียนการทดสอบสำหรับมันค่อนข้างง่าย: สิ่งที่คุณจำเป็นต้องตั้งค่าพารามิเตอร์การป้อนข้อมูล . การเขียนการทดสอบสำหรับread_file()และwrite_file()ค่อนข้างยากขึ้นเล็กน้อย (แม้ว่าฟังก์ชั่นนั้นแทบจะไม่สำคัญ) เพราะคุณจำเป็นต้องสร้างและ / หรืออ่านสิ่งต่าง ๆ บนระบบไฟล์ก่อนและหลังการเรียกใช้ฟังก์ชัน เป็นการดีที่คุณจะทำให้ฟังก์ชั่นดังกล่าวง่ายขึ้นจนคุณรู้สึกสบายใจที่จะไม่ทดสอบพวกเขาและทำให้คุณไม่ต้องยุ่งยากมากนัก

ความแตกต่างระหว่าง Python และ Haskell นี่คือ Haskell มีตัวตรวจสอบชนิดที่สามารถพิสูจน์ได้ว่าฟังก์ชั่นไม่มีผลข้างเคียง ใน Python คุณต้องหวังว่าจะไม่มีใครตกหล่นในการอ่านไฟล์หรือฟังก์ชั่นการเขียนทับโดยไม่ตั้งใจconvert()(พูด, read_config_file()) ใน Haskell เมื่อคุณประกาศconvert :: String -> Stringหรือคล้ายกันโดยไม่มีIOmonad ตัวตรวจสอบชนิดจะรับประกันได้ว่านี่เป็นฟังก์ชั่นแท้ที่ต้องอาศัยพารามิเตอร์อินพุตเท่านั้นและไม่มีอะไรอื่น หากมีคนพยายามแก้ไขconvertเพื่ออ่านไฟล์ปรับแต่งพวกเขาจะเห็นข้อผิดพลาดของคอมไพเลอร์อย่างรวดเร็วซึ่งแสดงว่าพวกเขากำลังทำลายความบริสุทธิ์ของฟังก์ชัน (และหวังว่าพวกเขาจะมีเหตุผลเพียงพอที่จะย้ายread_config_fileออกconvertและส่งผ่านผลลัพธ์ไปสู่การconvertรักษาความบริสุทธิ์)

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