ความเข้าใจผิดเกี่ยวกับภาษาที่ใช้งานได้จริงหรือ


39

ฉันมักจะพบข้อความ / ข้อโต้แย้งต่อไปนี้:

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

ฉันไม่ใช่ผู้เชี่ยวชาญในภาษาที่ใช้งานได้ แต่นี่คือสิ่งที่ฉันเข้าใจเกี่ยวกับหัวข้อเหล่านี้จนถึงปัจจุบัน

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

เกี่ยวกับจุดที่ 2 เท่าที่ฉันรู้ว่าคุณสามารถเป็นตัวแทนของรัฐโดยการทำเกลียวค่าผ่านขั้นตอนการคำนวณต่าง ๆ (ใน Haskell อีกครั้งโดยใช้ประเภท monadic) แต่ฉันไม่มีประสบการณ์เชิงปฏิบัติและความเข้าใจของฉันค่อนข้างคลุมเครือ

ดังนั้นข้อความทั้งสองข้างต้นถูกต้องในทุกแง่มุมหรือเป็นเพียงความเข้าใจผิดเกี่ยวกับภาษาที่ใช้งานได้จริงหรือ หากพวกเขาเข้าใจผิดพวกเขามาอย่างไร คุณสามารถเขียนข้อมูลโค้ด (อาจเล็ก) ที่แสดงวิธี Haskell สำนวนเพื่อ (1) ใช้ผลข้างเคียงและ (2) ใช้การคำนวณกับรัฐได้หรือไม่?


7
ฉันคิดว่าส่วนใหญ่บานพับนี้กับสิ่งที่คุณกำหนดภาษาการทำงาน 'บริสุทธิ์' ที่จะ
jk

@jk: เพื่อหลีกเลี่ยงปัญหาในการกำหนดภาษาที่ใช้งานได้ 'บริสุทธิ์' ให้ถือว่าความบริสุทธิ์ในความหมายของ Haskell (ซึ่งกำหนดไว้อย่างดี) ภาษาที่ใช้ในการทำงานนั้นถือได้ว่าเป็น 'บริสุทธิ์' ซึ่งอาจเป็นหัวข้อของคำถามในอนาคต
Giorgio

คำตอบทั้งสองนี้มีแนวคิดที่ชัดเจนมากมายและฉันก็ยากที่จะเลือกว่าจะยอมรับข้อใด ฉันตัดสินใจยอมรับคำตอบของ sepp2k เนื่องจากตัวอย่างรหัสเทียมเพิ่มเติม
Giorgio

คำตอบ:


26

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

ภาษาการเขียนโปรแกรมที่ใช้งานได้จริงไม่อนุญาตให้มีผลข้างเคียง (และมีประโยชน์น้อยในทางปฏิบัติเพราะโปรแกรมที่มีประโยชน์ใด ๆ มีผลข้างเคียงเช่นเมื่อมันโต้ตอบกับโลกภายนอก)

วิธีที่ง่ายที่สุดในการบรรลุความโปร่งใสในการอ้างอิงคือการไม่อนุญาตให้มีผลข้างเคียงและมีภาษาที่แน่นอนในกรณีนี้ (ส่วนใหญ่เป็นโดเมนเฉพาะ) อย่างไรก็ตามมันไม่ใช่วิธีเดียวและเป็นภาษาที่ใช้งานได้ทั่วๆไป ​​(Haskell, Clean, ... ) ทำให้เกิดผลข้างเคียง

นอกจากนี้ยังบอกว่าภาษาการเขียนโปรแกรมที่ไม่มีผลข้างเคียงคือการใช้งานในทางปฏิบัติน้อยมากฉันคิดว่าไม่ยุติธรรม - ไม่ใช่สำหรับภาษาเฉพาะโดเมน แต่สำหรับภาษาที่ใช้โดยทั่วไปฉันคิดว่าภาษาจะมีประโยชน์มากหากไม่มีผลข้างเคียง . อาจไม่ใช่สำหรับแอปพลิเคชั่นคอนโซล แต่ฉันคิดว่าแอปพลิเคชัน GUI สามารถนำไปใช้งานได้อย่างดีโดยไม่มีผลข้างเคียงในกระบวนทัศน์การทำงานแบบโต้ตอบ

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

นั่นเป็นวิธีที่ง่ายกว่า เพียงแค่มีระบบที่จำเป็นต้องทำเครื่องหมายฟังก์ชันที่มีผลข้างเคียง (คล้ายกับความถูกต้องของ const ใน C ++ แต่ด้วยผลข้างเคียงทั่วไป) ไม่เพียงพอที่จะทำให้มั่นใจถึงความโปร่งใสในการอ้างอิง คุณต้องแน่ใจว่าโปรแกรมไม่สามารถเรียกใช้ฟังก์ชันได้หลายครั้งด้วยอาร์กิวเมนต์เดียวกันและรับผลลัพธ์ที่แตกต่างกัน คุณสามารถทำได้โดยการทำสิ่งต่าง ๆ เช่นreadLineเป็นสิ่งที่ไม่ใช่ฟังก์ชั่น (นั่นคือสิ่งที่ Haskell ทำกับ IO monad) หรือคุณอาจทำให้เป็นไปไม่ได้ที่จะเรียกใช้ฟังก์ชันที่มีผลกระทบด้านข้างหลายครั้งด้วยอาร์กิวเมนต์เดียวกัน (นั่นคือสิ่งที่ Clean ทำ) ในกรณีหลังคอมไพเลอร์จะตรวจสอบให้แน่ใจว่าทุกครั้งที่คุณเรียกฟังก์ชั่นผลกระทบด้านข้างคุณทำเช่นนั้นด้วยอาร์กิวเมนต์ใหม่และมันจะปฏิเสธโปรแกรมใด ๆ ที่คุณส่งอาร์กิวเมนต์เดียวกันไปยังฟังก์ชันผลกระทบข้างเคียงสองครั้ง

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

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

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

หากพวกเขาเข้าใจผิดพวกเขามาอย่างไร

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

นอกจากนี้สถานะที่เปลี่ยนแปลงไม่ได้นั้นเป็นอุปสรรคอย่างมากในภาษาที่ใช้งานได้ดังนั้นจึงไม่เป็นการเผ่นเลยที่จะคิดว่ามันไม่ได้รับอนุญาตให้ใช้ในภาษาที่ทำงานได้อย่างแท้จริง

คุณสามารถเขียนข้อมูลโค้ด (อาจเล็ก) ที่แสดงวิธี Haskell สำนวนเพื่อ (1) ใช้ผลข้างเคียงและ (2) ใช้การคำนวณกับรัฐได้หรือไม่?

นี่คือแอปพลิเคชันใน Pseudo-Haskell ที่ขอชื่อผู้ใช้และทักทายเขา หลอก Haskell เป็นภาษาที่ฉันเพิ่งคิดค้นซึ่งมีระบบ IO Haskell แต่ใช้ไวยากรณ์ธรรมดาชื่อฟังก์ชั่นที่สื่อความหมายมากขึ้นและไม่เคยมีใครdo-notation (เป็นที่จะหันเหความสนใจจากเพียงวิธีการที่ตรง IO monad งาน):

greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)

เงื่อนงำที่นี่คือreadLineค่าประเภทIO<String>และcomposeMonadเป็นฟังก์ชันที่รับอาร์กิวเมนต์ชนิดIO<T>(สำหรับบางประเภทT) และอาร์กิวเมนต์อื่นที่เป็นฟังก์ชันที่รับอาร์กิวเมนต์ชนิดTและคืนค่าประเภทIO<U>(สำหรับบางประเภทU) เป็นฟังก์ชั่นที่ใช้เวลาสตริงและส่งกลับค่าชนิดprintIO<void>

ค่าของประเภทIO<A>เป็นค่าที่ "ถอดรหัส" Aการดำเนินการให้ที่ก่อให้เกิดค่าของชนิด composeMonad(m, f)ผลิตใหม่IOค่าที่เข้ารหัสการกระทำของmตามมาด้วยการกระทำของf(x)ซึ่งเป็นค่าที่ผลิตโดยการดำเนินการการกระทำของxm

รัฐที่ไม่แน่นอนจะมีลักษณะเช่นนี้:

counter = mutableVariable(0)
increaseCounter(cnt) =
    setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
    composeMonad(getValue(cnt), setIncreasedValue)

printCounter(cnt) = composeMonad( getValue(cnt), print )

main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )

นี่mutableVariableคือฟังก์ชั่นที่ใช้ค่าของชนิดใด ๆและผลิตT MutableVariable<T>ฟังก์ชั่นgetValueใช้MutableVariableและส่งกลับค่าIO<T>ที่สร้างมูลค่าปัจจุบัน setValueรับ a MutableVariable<T>และ a Tและคืนค่าIO<void>ที่กำหนดค่า composeVoidMonadเหมือนกับcomposeMonadยกเว้นว่าอาร์กิวเมนต์แรกคือสิ่งIOที่ไม่สร้างมูลค่าที่สมเหตุสมผลและอาร์กิวเมนต์ที่สองคือ monad อื่นไม่ใช่ฟังก์ชันที่ส่งคืน monad

ในแฮสเค็ลล์มีน้ำตาลประโยคที่ทำให้ความเจ็บปวดเจ็บปวดน้อยลง แต่ก็ยังเห็นได้ชัดว่าสถานะที่เปลี่ยนแปลงไม่ได้นั้นเป็นสิ่งที่ภาษาไม่ต้องการให้คุณทำ


คำตอบที่ยอดเยี่ยมให้ความกระจ่างกับแนวคิดมากมาย ควรบรรทัดสุดท้ายของโค้ดใช้ชื่อcounterคือincreaseCounter(counter)?
Giorgio

@Giorgio ใช่มันควรจะเป็น แก้ไขแล้ว.
sepp2k

1
@Giorgio สิ่งหนึ่งที่ฉันลืมพูดถึงอย่างชัดเจนในโพสต์ของฉันคือการกระทำ IO กลับโดยmainจะเป็นสิ่งที่ได้รับการดำเนินการจริง นอกเหนือจากการส่งคืน IO จากmainไม่มีวิธีการดำเนินIOการกระทำ (โดยไม่ต้องใช้ฟังก์ชั่นความชั่วร้ายที่น่ากลัวที่มีunsafeในชื่อของพวกเขา)
sepp2k

ตกลง. Scarfridge ยังกล่าวถึงIOค่าทำลาย ฉันไม่เข้าใจว่าเขาอ้างถึงการจับคู่รูปแบบหรือไม่นั่นคือความจริงที่ว่าคุณสามารถแยกแยะค่าของประเภทข้อมูลพีชคณิตได้ แต่ไม่มีใครสามารถใช้การจับคู่รูปแบบเพื่อทำสิ่งนี้กับIOค่าได้
Giorgio

16

IMHO คุณกำลังสับสนเพราะมีความแตกต่างระหว่างบริสุทธิ์ภาษาและบริสุทธิ์ฟังก์ชั่น ให้เราเริ่มด้วยฟังก์ชั่น ฟังก์ชั่นนั้นบริสุทธิ์ถ้ามันจะ (รับอินพุตเดียวกัน) จะคืนค่าเดิมเสมอและไม่ทำให้เกิดผลข้างเคียงใด ๆ ที่สังเกตได้ ตัวอย่างทั่วไปคือฟังก์ชันทางคณิตศาสตร์เช่น f (x) = x * x ตอนนี้ให้พิจารณาการนำไปใช้ของฟังก์ชันนี้ มันจะบริสุทธิ์ในภาษาส่วนใหญ่แม้จะไม่ได้รับการพิจารณาว่าเป็นภาษาที่ใช้งานได้จริงเช่น ML แม้แต่วิธีการ Java หรือ C ++ ที่มีพฤติกรรมนี้ก็ถือว่าเป็นเรื่องที่บริสุทธิ์

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

ถ้าคุณต้องการสถานะภายใน คุณสามารถเลียนแบบสถานะในภาษาบริสุทธิ์เพียงแค่เพิ่มสถานะก่อนการคำนวณเป็นพารามิเตอร์อินพุตและสถานะหลังจากการคำนวณเป็นส่วนหนึ่งของผลลัพธ์ แทนการที่คุณได้รับสิ่งที่ต้องการInt -> Bool Int -> State -> (Bool, State)คุณเพียงแค่ทำให้การพึ่งพาชัดเจน (ซึ่งถือเป็นแนวปฏิบัติที่ดีในกระบวนทัศน์การเขียนโปรแกรมใด ๆ ) BTW มี monad ที่เป็นวิธีที่หรูหราโดยเฉพาะอย่างยิ่งในการรวมฟังก์ชั่นการเลียนแบบรัฐเข้ากับฟังก์ชั่นการเลียนแบบรัฐที่ใหญ่กว่า วิธีนี้คุณสามารถ "รักษาสถานะ" ในภาษาบริสุทธิ์ได้อย่างแน่นอน แต่คุณต้องทำให้ชัดเจน

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

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

ใน Haskell สามารถทำได้ด้วยประเภท IO คุณไม่สามารถทำลายผลลัพธ์ของ IO (โดยไม่มีกลไกที่ไม่ปลอดภัย) ดังนั้นคุณสามารถประมวลผลผลลัพธ์ IO ได้ด้วยฟังก์ชั่นที่กำหนดไว้ในโมดูล IO เท่านั้น โชคดีที่มี combinators ที่ยืดหยุ่นมากซึ่งช่วยให้คุณรับผล IO และประมวลผลในฟังก์ชันตราบใดที่ฟังก์ชันนั้นส่งคืนผลลัพธ์ IO อื่น Combinator นี้เรียกว่าผูก (หรือ>>=) IO a -> (a -> IO b) -> IO bและมีประเภท หากคุณพูดถึงแนวคิดนี้คุณจะไปถึงชั้นเรียน Monad และ IO เป็นตัวอย่างของมัน


4
ฉันไม่เห็นว่า Haskell (เพิกเฉยฟังก์ชั่นใด ๆunsafeในชื่อของมัน) ไม่ตรงตามนิยามอุดมคติของคุณ ไม่มีฟังก์ชั่นไม่บริสุทธิ์ใน Haskell (ไม่ต้องสนใจอีกต่อunsafePerformIOไป)
sepp2k

4
readFileและwriteFileจะส่งคืนIOค่าเดิมเสมอโดยให้อาร์กิวเมนต์เดียวกัน ดังนั้นตัวอย่างโค้ดสองอันlet x = writeFile "foo.txt" "bar" in x >> xและwriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"จะทำแบบเดียวกัน
sepp2k

3
@AidanCully คุณหมายถึงอะไรโดย "ฟังก์ชั่น IO"? ฟังก์ชั่นที่คืนค่าประเภทIO Somethingหรือไม่? ถ้าเป็นเช่นนั้นมันเป็นไปได้อย่างสมบูรณ์ที่จะเรียกใช้ฟังก์ชัน IO สองครั้งด้วยอาร์กิวเมนต์เดียวกัน: putStrLn "hello" >> putStrLn "hello"- ทั้งคู่เรียกว่าputStrLnมีอาร์กิวเมนต์เดียวกัน แน่นอนว่าไม่ใช่ปัญหาเพราะอย่างที่ผมบอกไปแล้วว่าการโทรทั้งสองจะส่งผลให้มีค่า IO เท่ากัน
sepp2k

3
@scarfridge การประเมินwriteFile "foo.txt" "bar"ไม่สามารถทำให้เกิดข้อผิดพลาดได้เนื่องจากการประเมินการเรียกใช้ฟังก์ชันไม่สามารถดำเนินการได้ หากคุณกำลังบอกว่าในตัวอย่างก่อนหน้าของฉันเวอร์ชันที่letมีเพียงโอกาสเดียวเท่านั้นที่จะทำให้เกิดความล้มเหลวของ IO ในขณะที่เวอร์ชันที่ไม่มีletมีสองแสดงว่าคุณผิด ทั้งสองเวอร์ชันมีสองโอกาสสำหรับความล้มเหลวของ IO เนื่องจากletรุ่นจะประเมินการเรียกใช้writeFileเพียงครั้งเดียวในขณะที่รุ่นนั้นไม่มีการletประเมินสองครั้งคุณจะเห็นว่าไม่สำคัญว่าจะเรียกใช้ฟังก์ชันบ่อยเพียงใด มันมีความสำคัญเฉพาะกับผลลัพธ์ที่เกิดขึ้นบ่อยครั้ง ...
sepp2k

6
@AidanCully "กลไก monad" ไม่ผ่านพารามิเตอร์โดยปริยาย ฟังก์ชั่นใช้เวลาตรงหนึ่งอาร์กิวเมนต์ซึ่งเป็นประเภทputStrLn Stringหากคุณไม่เชื่อฉันให้ดูที่ประเภทของมัน: String -> IO (). แน่นอนมันไม่ได้มีชนิดของข้อโต้แย้งใด ๆIO- มันสร้างมูลค่าของประเภทนั้น
sepp2k
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.