ภาษาการเขียนโปรแกรมเชิงฟังก์ชันทำงานอย่างไร


92

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

ตัวอย่างเช่นสิ่งที่ C ง่ายๆนี้จะแปลเป็นภาษาโปรแกรมที่ใช้งานได้เช่น Haskell ได้อย่างไร

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(คำถามของฉันได้รับแรงบันดาลใจจากโพสต์ที่ยอดเยี่ยมนี้: "Execution in the Kingdom of Nouns" การอ่านมันทำให้ฉันเข้าใจดีขึ้นว่าการเขียนโปรแกรมเชิงวัตถุคืออะไร Java ใช้งานอย่างไรในลักษณะที่รุนแรงและภาษาโปรแกรมที่ใช้งานได้นั้นเป็นอย่างไร ตรงกันข้าม)


เป็นไปได้ที่ซ้ำกันของstackoverflow.com/questions/1916692/…
Chris Conway

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

14
SPJ ไม่ได้พูดถึงผลข้างเคียงที่นั่นไม่ใช่รัฐ? การคำนวณแบบเพียวมีเงื่อนไขมากมายในการผูกอาร์กิวเมนต์และ call stack แต่ไม่มีผลข้างเคียง (เช่น I / O) ไม่สามารถทำอะไรที่เป็นประโยชน์ได้ ทั้งสองจุดมีความแตกต่างกันมาก - มีรหัส Haskell ที่บริสุทธิ์และมีสถานะStateเป็นจำนวนมากและmonad นั้นสง่างามมาก ในทางกลับกันIOเป็นแฮ็คที่น่าเกลียดและสกปรกที่ใช้อย่างไม่พอใจเท่านั้น
CA McCann

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

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

คำตอบ:


80

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

ในขณะที่คุณรวบรวมโปรแกรมการทำงานจะไม่มีสถานะ แต่ไม่ได้หมายความว่าจะไม่สามารถจัดเก็บข้อมูลได้ ความแตกต่างคือถ้าฉันเขียนคำสั่ง (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 ราวกับว่ามันเป็นภาษาที่ไม่บริสุทธิ์! แต่จำไว้ว่ามีการแสดงหน้าที่อยู่: ไม่มีใครโกง

(ป.ล. : ขออภัยเกี่ยวกับความยาวฉันไปไกลเล็กน้อย)


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

19
นีล: จริงเหรอ? ฉันพบว่าไวยากรณ์ของ Haskell สะอาดมากจริงๆ ฉันอยากรู้; คุณหมายถึงอะไรเป็นพิเศษ? (สำหรับสิ่งที่คุ้มค่า C ++ ก็ไม่ทำให้ฉันรำคาญเช่นกันยกเว้นว่าต้องทำ> >ในเทมเพลต)
Antal Spector-Zabusky

6
ในสายตาของฉันในขณะที่ไวยากรณ์ของ Haskell ไม่สะอาดเท่าพูด Scheme แต่ก็ไม่ได้เริ่มเปรียบเทียบกับไวยากรณ์ที่น่าเกลียดแม้แต่ภาษาที่ดีที่สุดในภาษาปีกกาซึ่ง C ++ ก็อยู่ในกลุ่มที่แย่ที่สุด . ฉันคิดว่าไม่มีการบัญชีสำหรับรสชาติ ฉันไม่คิดว่าจะมีภาษาที่ทุกคนคิดว่าน่าพอใจ
CA McCann

8
@NeilButterworth: ฉันสงสัยว่าปัญหาของคุณไม่ใช่ไวยากรณ์มากเท่ากับชื่อฟังก์ชัน หากฟังก์ชันเหมือน>>=หรือ$มีมากกว่าที่เรียกแทนbindและapplyโค้ด haskell จะดูเหมือน perl น้อยกว่ามาก ฉันหมายถึงความแตกต่างหลักระหว่างไวยากรณ์ haskell และแบบแผนคือ haskell มีตัวดำเนินการ infix และ parens ที่เป็นทางเลือก หากผู้คนละเว้นจากการใช้ตัวดำเนินการ infix มากเกินไป Haskell จะดูเหมือนโครงการที่มีค่าใช้จ่ายน้อยกว่า
sepp2k

5
@camcann: ดีจุด (functionName arg1 arg2)แต่สิ่งที่ผมหมายถึงคือไวยากรณ์พื้นฐานของโครงการคือ หากคุณลบ parens functionName arg1 arg2ซึ่งเป็นไวยากรณ์ของ haskell หากคุณอนุญาตให้ใช้ตัวดำเนินการ infix ที่มีชื่อที่น่ากลัวตามอำเภอใจarg1 §$%&/*°^? arg2ซึ่งจะเหมือนกับ haskell (ฉันแค่ล้อเล่น btw จริง ๆ แล้วฉันชอบ haskell)
sepp2k

23

คำตอบดีๆมากมายที่นี่ แต่มันยาว ฉันจะพยายามให้คำตอบสั้น ๆ ที่เป็นประโยชน์:

  • ภาษาที่ใช้งานได้วางสถานะไว้ในตำแหน่งเดียวกับที่ C ทำ: ในตัวแปรที่ตั้งชื่อและในวัตถุที่จัดสรรบนฮีป ความแตกต่างคือ:

    • ในภาษาทำงานเป็น "ตัวแปร" ได้รับค่าเริ่มต้นของมันเมื่อมันเข้ามาในขอบเขต (ผ่านการเรียกใช้ฟังก์ชันหรือให้มีผลผูกพัน) และค่าที่ไม่เปลี่ยนแปลงในภายหลัง ในทำนองเดียวกันอ็อบเจ็กต์ที่จัดสรรบนฮีปจะเริ่มต้นทันทีด้วยค่าของฟิลด์ทั้งหมดซึ่งจะไม่เปลี่ยนแปลงหลังจากนั้น

    • "การเปลี่ยนแปลงสถานะ" ไม่ได้จัดการโดยการกลายพันธุ์ตัวแปรหรือวัตถุที่มีอยู่ แต่โดยการผูกตัวแปรใหม่หรือจัดสรรวัตถุใหม่

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

    • ไม่มีโปรแกรมใดสามารถสร้างสำเนาของโลกได้ (คุณจะวางไว้ที่ไหน)

    • ไม่มีโปรแกรมใดสามารถทิ้งโลกได้

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

    เคล็ดลับนี้จะมีการอธิบายอย่างสวยงามโดยไซมอนเพย์ตันโจนส์และฟิล Wadler ในกระดาษสถานที่สำคัญของพวกเขา"ความจำเป็นหน้าที่ Programming"


4
เท่าที่ฉันสามารถบอกได้IOเรื่องนี้( World -> (a,World)) เป็นตำนานเมื่อนำไปใช้กับ Haskell เนื่องจากแบบจำลองนั้นอธิบายเฉพาะการคำนวณตามลำดับเท่านั้นในขณะที่IOประเภทของ Haskell มีการทำงานพร้อมกัน โดย "ลำดับอย่างหมดจด" ฉันหมายความว่าแม้โลก (จักรวาล) จะไม่ได้รับอนุญาตให้เปลี่ยนแปลงระหว่างจุดเริ่มต้นและจุดสิ้นสุดของการคำนวณที่จำเป็นนอกเหนือจากการคำนวณนั้น ตัวอย่างเช่นในขณะที่คอมพิวเตอร์ของคุณกำลังทำงานอยู่สมองของคุณก็ไม่สามารถทำได้ ภาวะพร้อมกันสามารถจัดการได้ด้วยสิ่งที่ต้องการมากกว่าWorld -> PowerSet [(a,World)]ซึ่งช่วยให้เกิดการไม่ยอมรับและการแทรกแซง
Conal

1
@Conal: ฉันคิดว่าเรื่องราวของ IO เป็นเรื่องที่เข้าใจได้ดีกับการไม่ยอมรับและการสอดแทรก ถ้าจำไม่ผิดมีคำอธิบายที่ค่อนข้างดีในกระดาษ "ทีมที่น่าอึดอัด" แต่ฉันไม่รู้กระดาษดีๆที่อธิบายความเท่าเทียมที่แท้จริงได้อย่างชัดเจน
Norman Ramsey

3
ตามที่ฉันเข้าใจกระดาษ "ทีมที่น่าอึดอัด" ละทิ้งความพยายามที่จะสรุปรูปแบบการบอกเลิกอย่างง่าย ๆIOนั่นคือWorld -> (a,World)("ตำนาน" ที่เป็นที่นิยมและคงอยู่ที่ฉันอ้างถึง) และให้คำอธิบายเชิงปฏิบัติการแทน บางคนชอบความหมายเชิงปฏิบัติการ แต่พวกเขาทำให้ฉันไม่พอใจอย่างมาก โปรดดูคำตอบที่ยาวกว่าของฉันในคำตอบอื่น
Conal

+1 สิ่งนี้ช่วยให้ฉันเข้าใจ IO Monads มากขึ้นและตอบคำถามได้
CaptainCasey

คอมไพเลอร์ Haskell ส่วนใหญ่กำหนดIOเป็นRealWorld -> (a,RealWorld)แต่แทนที่จะเป็นตัวแทนของโลกแห่งความเป็นจริงมันเป็นเพียงค่านามธรรมที่ต้องผ่านไปรอบ ๆ และลงเอยด้วยการปรับให้เหมาะสมโดยคอมไพเลอร์
Jeremy List

19

ฉันกำลังทำลายความคิดเห็นตอบกลับคำตอบใหม่เพื่อให้มีพื้นที่มากขึ้น:

ฉันเขียน:

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

Norman เขียนว่า:

@Conal: ฉันคิดว่าเรื่องราวของ IO เป็นเรื่องที่เข้าใจได้ดีสำหรับการไม่ยอมรับและการสอดแทรก ถ้าจำไม่ผิดมีคำอธิบายที่ค่อนข้างดีในกระดาษ "ทีมที่น่าอึดอัด" แต่ฉันไม่รู้กระดาษดีๆที่อธิบายความเท่าเทียมกันอย่างชัดเจน

@ Norman: สรุปในแง่อะไร? ฉันขอแนะนำว่ารูปแบบ / คำอธิบายที่ระบุมักจะได้รับWorld -> (a,World)ไม่ตรงกับ Haskell IOเนื่องจากไม่ได้คำนึงถึงการไม่ยอมรับและการเกิดพร้อมกัน อาจมีโมเดลที่ซับซ้อนกว่านี้ที่เหมาะสมเช่นWorld -> PowerSet [(a,World)]แต่ฉันไม่รู้ว่าโมเดลดังกล่าวได้รับการออกแบบมาหรือไม่และแสดงว่าเพียงพอและสอดคล้องกันหรือไม่ โดยส่วนตัวแล้วฉันสงสัยว่าสัตว์ชนิดนี้สามารถพบได้เนื่องจากIOมีการเรียก API ที่จำเป็นซึ่งนำเข้าจาก FFI หลายพันรายการ และด้วยเหตุIOนี้จึงบรรลุวัตถุประสงค์:

เปิดปัญหา: IOmonad กลายเป็นบาปของ Haskell (เมื่อใดก็ตามที่เราไม่เข้าใจบางสิ่งเราจะโยนมันลงใน IO monad)

(จากการพูดคุย POPL ของ Simon PJ การสวมเสื้อคลุมผมการสวมเสื้อคลุมผม: ย้อนหลังใน Haskell )

ในส่วนที่ 3.1 ของการต่อสู้กับทีมที่น่าอึดอัด Simon ชี้ให้เห็นถึงสิ่งที่ไม่ได้ผลtype IO a = World -> (a, World)รวมถึง "วิธีการนี้ไม่ได้ปรับขนาดได้ดีเมื่อเราเพิ่มการทำงานพร้อมกัน" จากนั้นเขาก็แนะนำรูปแบบทางเลือกที่เป็นไปได้จากนั้นก็ละทิ้งความพยายามในการอธิบายแบบบอกเลิกโดยกล่าวว่า

อย่างไรก็ตามเราจะนำความหมายเชิงปฏิบัติการมาใช้แทนโดยยึดตามแนวทางมาตรฐานของความหมายของนิ่วในกระบวนการ

ความล้มเหลวในการค้นหารูปแบบการแสดงนัยที่แม่นยำและมีประโยชน์นี้เป็นรากฐานของสาเหตุที่ฉันเห็นว่า Haskell IO เป็นการละทิ้งจากจิตวิญญาณและประโยชน์ที่ลึกซึ้งของสิ่งที่เราเรียกว่า . ดูความคิดเห็นที่นี่


ขอบคุณสำหรับคำตอบที่ยาวขึ้น ฉันคิดว่าบางทีฉันอาจถูกล้างสมองโดยหัวหน้าฝ่ายปฏิบัติการคนใหม่ของเรา ตัวย้ายซ้ายและตัวย้ายขวาและอื่น ๆ ทำให้สามารถพิสูจน์ทฤษฎีบทที่มีประโยชน์บางอย่างได้ คุณจะได้เห็นใด ๆรูปแบบ denotational คุณชอบบัญชีที่ไม่นิยมและเห็นพ้องด้วย? ฉันไม่ได้.
Norman Ramsey

1
ฉันชอบวิธีที่World -> PowerSet [World]จับภาพความไม่เชื่อมั่นและการเกิดพร้อมกันแบบสอดแทรกได้อย่างชัดเจน คำจำกัดความของโดเมนนี้บอกฉันว่าการเขียนโปรแกรมที่จำเป็นพร้อมกันในกระแสหลัก (รวมถึงของ Haskell) นั้นไม่สามารถทำได้ยาก - แท้จริงแล้วมีความซับซ้อนมากกว่าลำดับ ความเสียหายที่ยิ่งใหญ่ที่ฉันเห็นในIOตำนานของHaskell กำลังบดบังความซับซ้อนโดยธรรมชาตินี้ซึ่งเป็นการลดทอนการโค่นล้ม
Conal

ในขณะที่ฉันเห็นว่าเหตุใดจึงWorld -> (a, World)เสีย แต่ฉันก็ไม่ชัดเจนว่าเหตุใดการแทนที่จึงสร้างWorld -> PowerSet [(a,World)]โมเดลการทำงานพร้อมกันได้อย่างถูกต้อง ฯลฯ สำหรับฉันแล้วดูเหมือนว่าจะบอกเป็นนัยว่าโปรแกรมในIOควรทำงานในบางอย่างเช่นรายการ monad โดยใช้ตัวเองกับทุกรายการของชุดที่ส่งคืน โดยการIOกระทำ ฉันขาดอะไรไป?
Antal Spector-Zabusky

3
@Absz: อย่างแรกโมเดลที่ฉันแนะนำWorld -> PowerSet [(a,World)]ไม่ถูกต้อง มาลองWorld -> PowerSet ([World],a)แทนกัน PowerSetให้ชุดของผลลัพธ์ที่เป็นไปได้ (nondeterminism) [World]เป็นลำดับของสถานะระดับกลาง (ไม่ใช่รายการ / nondeterminism monad) ซึ่งอนุญาตให้มีการแทรกระหว่างกัน (การตั้งเวลาเธรด) และ([World],a)ก็ไม่ถูกต้องเช่นกันเนื่องจากอนุญาตให้เข้าถึงaก่อนที่จะผ่านสถานะกลางทั้งหมด ให้กำหนดใช้World -> PowerSet (Computation a)ตำแหน่งแทนdata Computation a = Result a | Step World (Computation a)
Conal

ฉันยังไม่เห็นปัญหากับWorld -> (a, World). หากWorldประเภทนั้นรวมถึงโลกทั้งหมดจริงๆก็จะรวมข้อมูลเกี่ยวกับกระบวนการทั้งหมดที่ทำงานพร้อมกันและยังรวมถึง 'เมล็ดพันธุ์สุ่ม' ของสิ่งที่ไม่ใช่ปัจจัย ผลลัพธ์ที่ได้Worldคือโลกที่มีเวลาก้าวหน้าและมีปฏิสัมพันธ์บางอย่าง ปัญหาที่แท้จริงเพียงอย่างเดียวของโมเดลนี้ดูเหมือนจะกว้างเกินไปและWorldไม่สามารถสร้างและปรับแต่งค่าต่างๆได้
Rotsor

17

การเขียนโปรแกรมเชิงฟังก์ชันเกิดจากแคลคูลัสแลมบ์ดา หากคุณต้องการเข้าใจการเขียนโปรแกรมเชิงฟังก์ชันอย่างแท้จริงโปรดดูที่http://worrydream.com/AlligatorEggs/

มันเป็นวิธีที่ "สนุก" ในการเรียนรู้แลมด้าแคลคูลัสและนำคุณเข้าสู่โลกที่น่าตื่นเต้นของการเขียนโปรแกรมเชิงฟังก์ชัน!

การรู้แลมบ์ดาแคลคูลัสมีประโยชน์อย่างไรในการเขียนโปรแกรมเชิงฟังก์ชัน

Lambda Calculus จึงเป็นรากฐานสำหรับภาษาโปรแกรมในโลกแห่งความเป็นจริงมากมายเช่น Lisp, Scheme, ML, Haskell, ....

สมมติว่าเราต้องการอธิบายฟังก์ชั่นที่เพิ่มสามในอินพุตใด ๆ เพื่อทำเช่นนั้นเราจะเขียน:

plus3 x = succ(succ(succ x)) 

อ่าน“ plus3 เป็นฟังก์ชันที่เมื่อนำไปใช้กับจำนวน x ใด ๆ จะให้ผลตอบแทนของตัวตายตัวแทนของตัวต่อของ x”

โปรดทราบว่าฟังก์ชันที่เพิ่ม 3 ให้กับตัวเลขใด ๆ ไม่จำเป็นต้องมีชื่อว่า plus3 ชื่อ“ plus3” เป็นเพียงชวเลขที่สะดวกสำหรับการตั้งชื่อฟังก์ชันนี้

(plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))

สังเกตว่าเราใช้สัญลักษณ์แลมด้าสำหรับฟังก์ชัน (ฉันคิดว่ามันดูเหมือน Alligator ฉันเดาว่าแนวคิดสำหรับไข่จระเข้มาจากไหน)

สัญลักษณ์แลมบ์ดาคือAlligator (ฟังก์ชัน) และ x คือสีของมัน คุณยังสามารถคิดว่า x เป็นอาร์กิวเมนต์ (ฟังก์ชันแลมบ์ดาแคลคูลัสเป็นเพียงอาร์กิวเมนต์เดียวเท่านั้น) ส่วนที่เหลือคุณสามารถคิดว่ามันเป็นเนื้อหาของฟังก์ชัน

ตอนนี้พิจารณาสิ่งที่เป็นนามธรรม:

g ≡ λ f. (f (f (succ 0)))

อาร์กิวเมนต์ f ถูกใช้ในตำแหน่งฟังก์ชัน (ในการโทร) เราเรียกฟังก์ชันลำดับที่สูงขึ้นของ ga เนื่องจากใช้ฟังก์ชันอื่นเป็นอินพุต คุณสามารถนึกถึงฟังก์ชันอื่นที่เรียก f ว่า " ไข่ " ตอนนี้ใช้สองฟังก์ชันหรือ " Alligators " ที่เราสร้างขึ้นเราสามารถทำสิ่งนี้ได้:

(g plus3) = (λ f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

ถ้าคุณสังเกตคุณจะเห็นว่าจระเข้λ f ของเรากินλ x จระเข้ของเราจากนั้นλ x จระเข้และตาย จากนั้นจระเข้λ x ของเราก็ไปเกิดใหม่ในไข่จระเข้ของλ f จากนั้นทำซ้ำขั้นตอนและλ x Alligator ทางด้านซ้ายจะกินλ x Alligator อีกตัวทางด้านขวา

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

ดังนั้นคุณสามารถดูว่าคุณรู้จัก Lambda Calculus หรือไม่คุณจะเข้าใจว่า Functional Languages ​​ทำงานอย่างไร


@tuckster: ฉันเคยเรียนแคลคูลัสแลมบ์ดามาหลายครั้งแล้ว ... และใช่บทความ AlligatorEggs ก็สมเหตุสมผลสำหรับฉัน แต่ฉันไม่สามารถเชื่อมโยงสิ่งนั้นกับการเขียนโปรแกรมได้ สำหรับฉันตอนนี้แล็บด้าแคลคูลัสเป็นเหมือนทฤษฎีแยกต่างหากนั่นคือตรงนั้น แนวคิดของแคลคูลัสแลมบ์ดาใช้ในภาษาโปรแกรมอย่างไร?
Lazer

3
@eSKay: Haskell เป็นแคลคูลัสแลมบ์ดาโดยมีชั้นน้ำตาลวากยสัมพันธ์บาง ๆ เพื่อให้ดูเหมือนภาษาโปรแกรมทั่วไปมากขึ้น ภาษาตระกูลลิสป์นั้นคล้ายคลึงกับแคลคูลัสแลมบ์ดาที่ไม่ได้พิมพ์ซึ่งเป็นสิ่งที่ Alligator Eggs แสดงถึง แลมบ์ดาแคลคูลัสเองก็เป็นภาษาการเขียนโปรแกรมที่เรียบง่ายเช่นเดียวกับ
CA McCann

@eSKay: ฉันได้เพิ่มเล็กน้อยเกี่ยวกับความเกี่ยวข้องกับตัวอย่างบางส่วน ฉันหวังว่านี่จะช่วยได้!
PJT

หากคุณกำลังจะลบออกจากคำตอบของฉันโปรดแสดงความคิดเห็นเกี่ยวกับสาเหตุที่ฉันสามารถลองปรับปรุงคำตอบของฉันได้ ขอขอบคุณ.
PJT

14

เทคนิคในการจัดการสถานะใน Haskell นั้นตรงไปตรงมามาก และคุณไม่จำเป็นต้องเข้าใจ monads เพื่อจัดการกับมัน

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

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

Monads เป็นเครื่องมือหนึ่งที่ช่วยจัดระเบียบสิ่งนี้ แต่ monads ไม่ใช่วิธีแก้ปัญหา วิธีแก้ปัญหาคือการคิดเกี่ยวกับการแปลงสภาพแทนที่จะเป็นรัฐ

นอกจากนี้ยังใช้งานได้กับ I / O สิ่งที่เกิดขึ้นคือสิ่งนี้: แทนที่จะรับอินพุตจากผู้ใช้ที่มีคุณสมบัติเทียบเท่าโดยตรงscanfและจัดเก็บไว้ที่ใดที่หนึ่งคุณควรเขียนฟังก์ชันเพื่อบอกว่าคุณจะทำอะไรกับผลลัพธ์ของscanfถ้าคุณมีแล้วจึงส่งผ่าน ฟังก์ชันไปยัง I / O API นั่นคือสิ่งที่>>=เกิดขึ้นเมื่อคุณใช้IOmonad ใน Haskell ดังนั้นคุณไม่จำเป็นต้องจัดเก็บผลลัพธ์ของ I / O ใด ๆ เลยเพียงแค่เขียนโค้ดที่ระบุว่าคุณต้องการแปลงร่างอย่างไร


8

(ภาษาที่ใช้งานได้บางภาษาอนุญาตให้ใช้ฟังก์ชันที่ไม่บริสุทธิ์)

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

RealWorld pureScanf(RealWorld world, const char* format, ...);

ภาษาที่แตกต่างกันมีกลยุทธ์ที่แตกต่างกันในการแยกโลกออกจากโปรแกรมเมอร์ ตัวอย่างเช่น Haskell ใช้ monads เพื่อซ่อนworldอาร์กิวเมนต์


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

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

คุณรวมส่วนการแก้ไขเข้ากับการเรียกใช้ฟังก์ชันโดยปกติจะเปลี่ยนลูปเป็นการเรียกซ้ำ

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}

หรือแค่computeSumOfSquares min max = sum [x*x | x <- [min..max]];-)
fredoverflow

@ เฟรด: ความเข้าใจในรายการเป็นเพียงน้ำตาลในเชิงไวยากรณ์ (จากนั้นคุณต้องอธิบายรายละเอียดของรายการ monad) และคุณจะดำเนินการsumอย่างไร? ยังคงต้องมีการเรียกซ้ำ
kennytm

3

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

ยกตัวอย่างเช่นการตรวจสอบของ Haskell รัฐ Monad


9
และโปรดทราบว่าไม่มีอะไรเกี่ยวกับStateหรือMonadที่เปิดใช้งานสถานะเนื่องจากทั้งคู่ถูกกำหนดในรูปแบบของเครื่องมือที่ใช้งานง่ายทั่วไป พวกเขาเพียงจับรูปแบบที่เกี่ยวข้องดังนั้นคุณจึงไม่ต้องสร้างล้อใหม่มากนัก
Conal


1

haskell:

main = do no <- readLn
          print (no + 1)

แน่นอนคุณสามารถกำหนดสิ่งต่างๆให้กับตัวแปรในภาษาที่ใช้งานได้ คุณไม่สามารถเปลี่ยนแปลงได้ (โดยพื้นฐานแล้วตัวแปรทั้งหมดคือค่าคงที่ในภาษาที่ใช้งานได้)


@ sepp2k: ทำไมอะไรคืออันตรายในการเปลี่ยนแปลง?
Lazer

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

9
@eSKay: โปรแกรมเมอร์ที่ใช้งานได้เชื่อว่าสถานะที่เปลี่ยนแปลงได้ทำให้เกิดข้อบกพร่องมากมายและทำให้เหตุผลเกี่ยวกับพฤติกรรมของโปรแกรมทำได้ยากขึ้น ตัวอย่างเช่นหากคุณมีการเรียกใช้ฟังก์ชันf(x)และคุณต้องการดูว่าค่าของ x คืออะไรคุณก็ต้องไปที่ตำแหน่งที่กำหนด x ไว้ ถ้า x ไม่แน่นอนคุณจะต้องพิจารณาด้วยว่ามีจุดใดที่ x สามารถเปลี่ยนแปลงได้ระหว่างนิยามและการใช้งาน (ซึ่งไม่สำคัญถ้า x ไม่ใช่ตัวแปรโลคัล)
sepp2k

6
ไม่ใช่แค่โปรแกรมเมอร์ที่ใช้งานได้เท่านั้นที่ไม่ไว้วางใจสถานะที่ไม่แน่นอนและผลข้างเคียง วัตถุที่ไม่เปลี่ยนรูปและการแยกคำสั่ง / การสืบค้นได้รับการยกย่องอย่างดีจากโปรแกรมเมอร์ OO จำนวนไม่น้อยและเกือบทุกคนคิดว่าตัวแปรทั่วโลกที่ไม่แน่นอนเป็นความคิดที่ไม่ดี ภาษาเช่น Haskell เพียงแค่ใช้ความคิดเพิ่มเติมว่าส่วนใหญ่ ...
CA McCann

5
@eSKay: การกลายพันธุ์ไม่มากจนเป็นอันตราย แต่ปรากฎว่าถ้าคุณตกลงที่จะหลีกเลี่ยงการกลายพันธุ์การเขียนโค้ดที่เป็นโมดูลและนำกลับมาใช้ใหม่ได้ง่ายกว่ามาก หากไม่มีสถานะที่ไม่สามารถใช้ร่วมกันได้การเชื่อมต่อระหว่างส่วนต่างๆของโค้ดจะกลายเป็นเรื่องที่ชัดเจนและง่ายต่อการทำความเข้าใจและดูแลการออกแบบของคุณ John Hughes อธิบายเรื่องนี้ได้ดีกว่าที่ฉันทำได้ คว้ากระดาษของเขาทำไมเรื่องหน้าที่ Programming
Norman Ramsey
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.