นี่คือวิธีที่ Haskell ทำ: (ไม่ใช่สิ่งที่ตรงกันข้ามกับข้อความของ Lippert เนื่องจาก Haskell ไม่ใช่ภาษาเชิงวัตถุ)
คำเตือน: คำตอบที่ยืดยาวจากแฟนบอย Haskell ข้างหน้า
TL; DR
ตัวอย่างนี้แสดงให้เห็นว่า Haskell แตกต่างจาก C # อย่างไร แทนที่จะมอบหมายการขนส่งของการก่อสร้างโครงสร้างให้กับตัวสร้างมันจะต้องได้รับการจัดการในรหัสโดยรอบ ไม่มีวิธีใดสำหรับค่า null (หรือNothing
ใน Haskell) เพื่อครอบตัดค่าที่เราคาดหวังว่าค่าที่ไม่ใช่ค่า null เนื่องจากค่า null สามารถเกิดขึ้นได้เฉพาะในประเภท wrapper พิเศษที่เรียกว่าMaybe
ซึ่งไม่สามารถใช้แทน / แปลงได้โดยตรงเป็นปกติไม่ใช่ ประเภท nullable เพื่อที่จะใช้ค่าที่ถูกทำให้เป็นโมฆะโดยการห่อใน a Maybe
, ก่อนอื่นเราต้องแยกค่าโดยใช้การจับคู่รูปแบบซึ่งบังคับให้เราเบี่ยงเบนการควบคุมการไหลเข้าไปในสาขาที่เรารู้แน่นอนว่าเรามีค่าที่ไม่เป็นโมฆะ
ดังนั้น:
เราจะรู้ได้หรือไม่ว่าการอ้างอิงที่ไม่เป็นโมฆะนั้นไม่เคยภายใต้สถานการณ์ใด ๆ
ใช่. Int
และMaybe Int
เป็นสองประเภทที่แยกจากกันอย่างสมบูรณ์ หาNothing
ในธรรมดาInt
จะเปรียบได้กับการหาสตริง "ปลา" Int32
ใน
สิ่งที่เกี่ยวกับในตัวสร้างของวัตถุที่มีเขตข้อมูลที่ไม่เป็นโมฆะประเภทการอ้างอิง?
ไม่ใช่ปัญหา: ตัวสร้างค่าใน Haskell ไม่สามารถทำอะไรได้เลย แต่เอาค่าที่ได้รับมารวมเข้าด้วยกัน ตรรกะการเริ่มต้นทั้งหมดจะเกิดขึ้นก่อนที่ตัวสร้างจะถูกเรียก
สิ่งที่เกี่ยวกับใน finalizer ของวัตถุดังกล่าวที่วัตถุจะเสร็จสมบูรณ์เพราะรหัสที่ควรจะกรอกในการอ้างอิงโยนข้อยกเว้น?
ไม่มีผู้ผ่านเข้ารอบสุดท้ายใน Haskell ดังนั้นฉันจึงไม่สามารถพูดเรื่องนี้ได้ อย่างไรก็ตามคำตอบแรกของฉันยังคงมีอยู่
คำตอบแบบเต็ม :
Haskell ไม่มีค่าว่างและใช้Maybe
ชนิดข้อมูลเพื่อแสดงค่าว่าง อาจเป็นชนิดข้อมูล algabraic ที่กำหนดเช่นนี้:
data Maybe a = Just a | Nothing
สำหรับคนที่ไม่คุ้นเคยกับ Haskell ให้อ่านสิ่งนี้ว่า "A Maybe
is a a Nothing
or a Just a
" โดยเฉพาะ:
Maybe
เป็นตัวสร้างประเภท : มันสามารถคิด (ไม่ถูกต้อง) เป็นคลาสทั่วไป (ซึ่งa
เป็นตัวแปรประเภท) C # class Maybe<a>{}
เปรียบเทียบคือ
Just
เป็นตัวสร้างค่า : มันเป็นฟังก์ชั่นที่รับอาร์กิวเมนต์ชนิดหนึ่งa
และคืนค่าประเภทMaybe a
ที่มีค่า ดังนั้นรหัสจะคล้ายคลึงกับx = Just 17
int? x = 17;
Nothing
เป็นตัวสร้างค่าอื่น แต่ไม่รับอาร์กิวเมนต์และค่าMaybe
ส่งคืนจะไม่มีค่าอื่นใดนอกจาก "ไม่มี" x = Nothing
คล้ายกับint? x = null;
(สมมติว่าเราถูก จำกัดa
ใน Haskell Int
ซึ่งสามารถทำได้โดยการเขียนx = Nothing :: Maybe Int
)
เมื่อพื้นฐานของMaybe
ประเภทนั้นหมดไปแล้ว Haskell จะหลีกเลี่ยงปัญหาที่กล่าวถึงในคำถามของ OP ได้อย่างไร
ดี Haskell เป็นจริงที่แตกต่างจากส่วนใหญ่ของภาษาที่กล่าวถึงเพื่อให้ห่างไกลดังนั้นฉันจะเริ่มต้นด้วยการอธิบายหลักการใช้ภาษาไม่กี่ขั้นพื้นฐาน
ออกครั้งแรกใน Haskell, ทุกอย่างจะไม่เปลี่ยนรูป ทุกอย่าง ชื่ออ้างถึงค่าไม่ใช่ตำแหน่งหน่วยความจำที่สามารถเก็บค่าได้ (เพียงอย่างเดียวนี้เป็นแหล่งกำจัดข้อผิดพลาดอย่างใหญ่หลวง) ซึ่งแตกต่างจากใน C # ที่ประกาศตัวแปรและการมอบหมายเป็นสองการดำเนินงานแยกต่างหากในค่า Haskell จะถูกสร้างขึ้นโดยการกำหนดค่าของพวกเขา (เช่นx = 15
, y = "quux"
, z = Nothing
) ซึ่งไม่สามารถเปลี่ยนแปลง ดังนั้นรหัสเช่น:
ReferenceType x;
เป็นไปไม่ได้ใน Haskell ไม่มีปัญหาในการเตรียมใช้งานค่าnull
เนื่องจากทุกอย่างต้องถูกเตรียมใช้งานอย่างชัดเจนเป็นค่าเพื่อให้มีอยู่
ครั้งที่สองHaskell ไม่ได้เป็นภาษาเชิงวัตถุ : มันเป็นทำงานอย่างหมดจดภาษาจึงมีวัตถุในความเข้มงวดของคำว่าไม่มี แต่มีเพียงฟังก์ชัน (ตัวสร้างค่า) ที่รับอาร์กิวเมนต์และส่งคืนโครงสร้างที่รวมกัน
ถัดไปไม่มีรหัสลักษณะที่แน่นอน โดยสิ่งนี้ฉันหมายความว่าภาษาส่วนใหญ่มีรูปแบบดังนี้:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
พฤติกรรมของโปรแกรมแสดงเป็นชุดคำสั่ง ในภาษาเชิงวัตถุการประกาศคลาสและฟังก์ชั่นยังมีบทบาทอย่างมากในการไหลของโปรแกรม แต่เป็นสาระสำคัญ "เนื้อ" ของการดำเนินการของโปรแกรมใช้รูปแบบของชุดคำสั่งที่จะดำเนินการ
ใน Haskell นี่เป็นไปไม่ได้ แต่โฟลว์ของโปรแกรมจะถูกกำหนดโดยฟังก์ชันการโยง แม้แต่คำอธิบายที่ดูเป็นเรื่องจำเป็นdo
ก็แค่น้ำตาลเชิงประโยคสำหรับส่งผ่านฟังก์ชั่นนิรนามไปยัง>>=
ผู้ปฏิบัติงาน ฟังก์ชั่นทั้งหมดอยู่ในรูปแบบของ:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
ไหนbody-expression
สามารถเป็นอะไรก็ได้ที่ประเมินค่า เห็นได้ชัดว่ามีคุณสมบัติทางไวยากรณ์เพิ่มเติมที่มีอยู่ แต่ประเด็นหลักคือการขาดลำดับของงบอย่างสมบูรณ์
สุดท้ายและที่สำคัญที่สุดระบบการพิมพ์ของ Haskell นั้นเข้มงวดอย่างไม่น่าเชื่อ ถ้าฉันต้องสรุปปรัชญาการออกแบบส่วนกลางของระบบพิมพ์ของ Haskell ฉันจะพูดว่า: "ทำสิ่งต่าง ๆ ให้มากที่สุดเท่าที่จะเป็นไปได้ในการรวบรวมเวลา ไม่มีการแปลงโดยนัยใด ๆ (ต้องการเลื่อนระดับเป็นInt
a Double
หรือไม่ใช้fromIntegral
ฟังก์ชัน) สิ่งเดียวที่เป็นไปได้ที่จะมีค่าที่ไม่ถูกต้องเกิดขึ้นขณะใช้งานจริงคือการใช้งานPrelude.undefined
(ซึ่งเห็นได้ชัดว่าต้องอยู่ที่นั่นและไม่สามารถลบได้ )
ด้วยสิ่งเหล่านี้ในใจลองดูตัวอย่างของ "แตกหัก" ของอมรและลองแสดงรหัสนี้อีกครั้งใน Haskell ก่อนการประกาศข้อมูล (ใช้ไวยากรณ์บันทึกสำหรับเขตข้อมูลที่มีชื่อ):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( foo
และbar
เป็นฟังก์ชั่นการเข้าถึงไปยังฟิลด์ที่ไม่ระบุตัวตนที่นี่แทนที่จะเป็นฟิลด์จริง แต่เราสามารถละเว้นรายละเอียดนี้ได้)
ตัวNotSoBroken
สร้างค่าไม่สามารถดำเนินการใด ๆ นอกเหนือจาก a Foo
และ a Bar
(ซึ่งไม่เป็นโมฆะ) และสร้างNotSoBroken
จากสิ่งเหล่านั้น ไม่มีที่สำหรับวางโค้ดที่จำเป็นหรือแม้แต่กำหนดฟิลด์ด้วยตนเอง ตรรกะการกำหนดค่าเริ่มต้นทั้งหมดจะต้องเกิดขึ้นที่อื่นส่วนใหญ่มักจะอยู่ในฟังก์ชั่นเฉพาะของโรงงาน
ในตัวอย่างการสร้างBroken
ล้มเหลวเสมอ ไม่มีวิธีใดที่จะทำลายตัวNotSoBroken
สร้างค่าในลักษณะที่คล้ายกัน (ไม่มีที่ไหนเลยที่จะเขียนโค้ด) แต่เราสามารถสร้างฟังก์ชั่นจากโรงงานที่มีข้อบกพร่องในทำนองเดียวกัน
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(บรรทัดแรกคือการประกาศลายเซ็นประเภท: makeNotSoBroken
ใช้เวลาFoo
และBar
เป็นอาร์กิวเมนต์และสร้างกMaybe NotSoBroken
)
ประเภทผลตอบแทนต้องMaybe NotSoBroken
และไม่เพียงNotSoBroken
เพราะเราบอกว่ามันจะประเมินซึ่งเป็นตัวสร้างความคุ้มค่าNothing
Maybe
ประเภทจะไม่เข้าแถวถ้าเราเขียนอะไรที่ต่างออกไป
นอกเหนือจากการไม่มีจุดหมายอย่างแน่นอนฟังก์ชั่นนี้ไม่ได้บรรลุวัตถุประสงค์ที่แท้จริงอย่างที่เราจะเห็นเมื่อเราพยายามใช้ มาสร้างฟังก์ชั่นที่เรียกuseNotSoBroken
ว่า a NotSoBroken
เป็นอาร์กิวเมนต์:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBroken
ยอมรับ a NotSoBroken
เป็นอาร์กิวเมนต์และสร้าง a Whatever
)
และใช้มันอย่างนั้น:
useNotSoBroken (makeNotSoBroken)
ในภาษาส่วนใหญ่พฤติกรรมประเภทนี้อาจทำให้เกิดข้อยกเว้นตัวชี้โมฆะ ใน Haskell ประเภทไม่ตรงกับขึ้นmakeNotSoBroken
ส่งกลับMaybe NotSoBroken
แต่คาดว่าจะมีuseNotSoBroken
NotSoBroken
ประเภทเหล่านี้ไม่สามารถใช้แทนกันได้และรหัสไม่สามารถคอมไพล์ได้
ในการหลีกเลี่ยงปัญหานี้เราสามารถใช้case
คำสั่งแยกสาขาตามโครงสร้างของMaybe
ค่า (ใช้คุณลักษณะที่เรียกว่าการจับคู่รูปแบบ ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
เห็นได้ชัดว่าตัวอย่างนี้ต้องอยู่ในบริบทที่จะรวบรวมจริง แต่มันแสดงให้เห็นถึงพื้นฐานของวิธี Haskell จัดการ nullables นี่คือคำอธิบายทีละขั้นตอนของรหัสด้านบน:
- ครั้งแรกที่ได้รับการประเมินซึ่งรับประกันได้ว่าจะก่อให้เกิดค่าของชนิด
makeNotSoBroken
Maybe NotSoBroken
case
คำสั่งตรวจสอบโครงสร้างของค่านี้
- หากมีค่ารหัส
Nothing
"จัดการสถานการณ์ที่นี่" จะถูกประเมิน
- หากค่านั้นตรงกับค่าแทน
Just
จะมีการดำเนินการกับสาขาอื่น ให้สังเกตว่าประโยคที่ตรงกันนั้นระบุค่าเป็นJust
โครงสร้างและผูกNotSoBroken
ฟิลด์ภายในกับชื่ออย่างไร (ในกรณีนี้x
) x
สามารถใช้เช่นเดียวกับNotSoBroken
ค่าปกติที่เป็น
ดังนั้นการจับคู่รูปแบบจึงเป็นเครื่องมือที่มีประสิทธิภาพสำหรับการบังคับใช้ความปลอดภัยของประเภทเนื่องจากโครงสร้างของวัตถุนั้นผูกติดอยู่กับการแยกสาขาของการควบคุม
ฉันหวังว่านี่เป็นคำอธิบายที่เข้าใจยาก ถ้ามันไม่เข้าท่าให้กระโดดเข้าไปในLearn You A Haskell for Great Good! หนึ่งในบทเรียนภาษาออนไลน์ที่ดีที่สุดที่ฉันเคยอ่าน หวังว่าคุณจะเห็นความงามแบบเดียวกันในภาษานี้ที่ฉันทำ