ไวยากรณ์“ Just” ใน Haskell หมายความว่าอย่างไร


118

ฉันได้สำรวจอินเทอร์เน็ตเพื่อหาคำอธิบายที่แท้จริงว่าคำหลักนี้ทำอะไร ทุกบทช่วยสอนของ Haskell ที่ฉันได้ดูเพียงแค่เริ่มใช้มันแบบสุ่มและไม่เคยอธิบายว่ามันทำอะไร (และฉันได้ดูหลาย ๆ อย่าง)

นี่คือส่วนพื้นฐานของรหัสจากโลกแห่งความจริง HaskellJustที่ใช้ ฉันเข้าใจว่าโค้ดใช้ทำอะไร แต่ฉันไม่เข้าใจว่าจุดประสงค์หรือหน้าที่ของJustมันคืออะไร

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

จากสิ่งที่ฉันสังเกตเห็นมันเกี่ยวข้องกับการMaybeพิมพ์ แต่นั่นเป็นสิ่งที่ฉันเรียนรู้ได้ดีทีเดียว

คำอธิบายที่ดีเกี่ยวกับความJustหมายจะได้รับการชื่นชมมาก

คำตอบ:


211

จริงๆแล้วมันเป็นเพียงตัวสร้างข้อมูลธรรมดาที่ถูกกำหนดไว้ในPreludeซึ่งเป็นไลบรารีมาตรฐานที่นำเข้าโดยอัตโนมัติในทุกโมดูล

สิ่งที่อาจจะเป็นโครงสร้าง

คำจำกัดความมีลักษณะดังนี้:

data Maybe a = Just a
             | Nothing

ประกาศที่กำหนดประเภท, Maybe aซึ่งจะแปรตามตัวแปรประเภทซึ่งหมายความเพียงแค่ว่าคุณสามารถใช้มันด้วยวิธีใดในสถานที่ของaa

การสร้างและการทำลายล้าง

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

เนื่องจากNothingไม่มีพารามิเตอร์ชนิดเมื่อใช้เป็นตัวสร้างชื่อมันมีค่าคงที่เป็นสมาชิกประเภททุกประเภทMaybe a aแต่ตัวJustสร้างมีพารามิเตอร์ชนิดซึ่งหมายความว่าเมื่อใช้เป็นตัวสร้างมันจะทำหน้าที่เหมือนฟังก์ชันจากชนิดaถึงMaybe aกล่าวคือมีประเภทa -> Maybe a

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

ในการใช้Maybe aค่าในการจับคู่รูปแบบคุณต้องระบุรูปแบบสำหรับตัวสร้างแต่ละตัวดังนี้:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

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

สิ่งที่อาจหมายถึง

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

หลายภาษาในปัจจุบันจัดการค่า "ไม่มีค่า" ประเภทนี้ผ่านการอ้างอิง NULL Tony Hoare นักวิทยาศาสตร์คอมพิวเตอร์ที่มีชื่อเสียง (เขาเป็นผู้คิดค้น Quicksort และเป็นผู้ชนะรางวัล Turing Award) เป็นเจ้าของสิ่งนี้ในฐานะ"ความผิดพลาดพันล้านดอลลาร์" ของเขา ประเภทบางทีไม่ใช่วิธีเดียวในการแก้ไขปัญหานี้ แต่ได้รับการพิสูจน์แล้วว่าเป็นวิธีที่มีประสิทธิภาพ

อาจจะเป็น Functor

ความคิดของการเปลี่ยนประเภทหนึ่งไปยังอีกที่หนึ่งเช่นว่าการดำเนินการกับชนิดเก่าสามารถยังถูกเปลี่ยนการทำงานในรูปแบบใหม่เป็นแนวคิดที่อยู่เบื้องหลังระดับประเภท Haskell เรียกว่าFunctorซึ่งMaybe aมีตัวอย่างการใช้งานของ

Functorจัดเตรียมวิธีการที่เรียกว่าfmapซึ่งแมปฟังก์ชันที่มีช่วงค่าจากประเภทพื้นฐาน (เช่นInteger) ไปยังฟังก์ชันที่มีช่วงค่าจากประเภทที่ยกขึ้น (เช่นMaybe Integer) ฟังก์ชันที่แปลงfmapเพื่อทำงานกับMaybeค่าจะทำงานดังนี้:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

ดังนั้นหากคุณมีMaybe Integerค่าm_xและInt -> Intฟังก์ชันfคุณสามารถfmap f m_xนำฟังก์ชันไปใช้ได้fโดยตรงMaybe Integerโดยไม่ต้องกังวลว่าจะมีค่าจริงหรือไม่ ในความเป็นจริงคุณสามารถใช้ห่วงโซ่ของInteger -> Integerฟังก์ชันยกทั้งหมดกับMaybe Integerค่าและต้องกังวลเกี่ยวกับการตรวจสอบอย่างชัดเจนNothingเพียงครั้งเดียวเมื่อคุณทำเสร็จแล้ว

อาจจะเป็น Monad

ผมไม่แน่ใจว่าคุณจะคุ้นเคยกับแนวคิดของการที่Monadแต่คุณมีอย่างน้อยใช้IO aก่อนและประเภทลายเซ็นรูปลักษณ์ที่น่าทึ่งคล้ายกับIO a Maybe aแม้ว่าจะIOมีความพิเศษตรงที่จะไม่เปิดเผยตัวสร้างให้คุณและสามารถ "รัน" ได้โดยระบบรันไทม์ของ Haskell เท่านั้น แต่ก็ยังเป็นFunctorไฟล์Monad. ในความเป็นจริงมีความรู้สึกที่สำคัญซึ่ง a Monadเป็นเพียงลักษณะพิเศษที่Functorมีคุณสมบัติพิเศษบางอย่าง แต่นี่ไม่ใช่สถานที่ที่จะเข้าไปในนั้น

อย่างไรก็ตาม Monads ชอบIOประเภทแผนที่เป็นประเภทใหม่ที่แสดงถึง "การคำนวณที่ทำให้เกิดค่า" และคุณสามารถยกฟังก์ชันออกเป็นMonadประเภทได้ผ่านfmapฟังก์ชันที่เหมือนมากซึ่งเรียก liftMว่าฟังก์ชันปกติให้เป็น "การคำนวณที่ให้ผลลัพธ์เป็นค่าที่ได้จากการประเมินค่า ฟังก์ชั่น."

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

คุณอาจจะเขียนได้

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

หวังว่าคุณจะเข้าใจMaybeประเภทและตัวสร้างในตอนนี้ แต่หากยังมีอะไรไม่ชัดเจนโปรดแจ้งให้เราทราบ!


19
คำตอบที่ยอดเยี่ยม! ควรกล่าวถึงว่า Haskell มักใช้ในMaybeที่ที่ภาษาอื่น ๆ จะใช้nullหรือnil(มีสิ่งที่น่ารังเกียจNullPointerExceptionแฝงตัวอยู่ทุกมุม) ตอนนี้ภาษาอื่น ๆ ก็เริ่มใช้โครงสร้างนี้เช่นกัน: Scala as Optionและแม้แต่ Java 8 ก็มีOptionalประเภท
Landei

3
นี่คือคำอธิบายที่ยอดเยี่ยม คำอธิบายจำนวนมากที่ฉันได้อ่านคำใบ้เกี่ยวกับแนวคิดที่ว่า Just เป็นตัวสร้างสำหรับประเภทอาจจะ แต่ไม่มีใครทำให้มันชัดเจน
reem

@Landei ขอบคุณสำหรับคำแนะนำ ฉันทำการแก้ไขเพื่ออ้างถึงอันตรายของการอ้างอิงที่เป็นโมฆะ
Levi Pearson

@Landei ประเภทตัวเลือกมีมาตั้งแต่ ML ในยุค 70 มีแนวโน้มว่า Scala จะหยิบมันขึ้นมาจากที่นั่นเพราะ Scala ใช้หลักการตั้งชื่อของ ML ตัวเลือกที่มีตัวสร้างบางตัวและไม่มีเลย
stonemetal

@Landei Apple Swift ใช้ตัวเลือกค่อนข้างน้อยเช่นกัน
Jamin

37

คำตอบส่วนใหญ่ในปัจจุบันเป็นคำอธิบายทางเทคนิคขั้นสูงเกี่ยวกับวิธีการJustทำงานของเพื่อน ๆ ฉันคิดว่าฉันอาจจะลองอธิบายว่ามันมีไว้เพื่ออะไร

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

Haskell แก้ปัญหานี้โดยกำหนดให้คุณทำเครื่องหมายสถานที่ที่คุณสามารถมีNothing(เวอร์ชัน a null) ได้อย่างชัดเจน โดยทั่วไปหากฟังก์ชันของคุณส่งคืนประเภทตามปกติFooควรส่งคืนประเภทMaybe Fooแทน Nothingหากคุณต้องการที่จะแสดงให้เห็นว่าไม่มีค่าตอบแทน หากคุณต้องการที่จะส่งกลับค่าคุณควรกลับbarJust bar

ดังนั้นโดยทั่วไปถ้าคุณไม่สามารถมีคุณไม่จำเป็นที่จะต้องNothing Justถ้าคุณสามารถมีคุณจำเป็นต้องทำNothingJust

ไม่มีอะไรวิเศษเกี่ยวกับMaybe; สร้างขึ้นจากระบบประเภท Haskell นั่นหมายความว่าคุณสามารถใช้เทคนิคการจับคู่รูปแบบ Haskell ตามปกติทั้งหมดได้


1
คำตอบที่ดีมากถัดจากคำตอบอื่น ๆ แต่ฉันคิดว่ามันจะยังคงได้รับประโยชน์จากตัวอย่างโค้ด :)
PascalVKooten

13

ระบุประเภทtค่าเป็นมูลค่าJust tที่มีอยู่ของประเภทtซึ่งNothingแสดงถึงความล้มเหลวในการเข้าถึงค่าหรือกรณีที่การมีค่าจะไม่มีความหมาย

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

สำหรับอีกตัวอย่างหนึ่งสิ่งนี้สามารถใช้ในการหารกำหนดฟังก์ชันการหารที่ใช้เวลาaและbและส่งกลับJust a/bหากbไม่ใช่ศูนย์หรือNothingอื่น ๆ มักใช้แบบนี้เป็นทางเลือกที่สะดวกในการยกเว้นหรือเช่นเดียวกับตัวอย่างก่อนหน้านี้เพื่อแทนที่ค่าที่ไม่สมเหตุสมผล


1
สมมติว่าในโค้ดด้านบนฉันลบ Just ทำไมถึงใช้ไม่ได้ คุณต้องมี Just ทุกเวลาที่คุณต้องการมี Nothing หรือไม่? จะเกิดอะไรขึ้นกับนิพจน์ที่ฟังก์ชันภายในนิพจน์นั้นส่งกลับ Nothing?
reem

7
หากคุณลบออกJustรหัสของคุณจะไม่พิมพ์ตรวจสอบ เหตุผลJustคือการรักษาประเภทที่เหมาะสม มีประเภทหนึ่ง (เป็น monad จริง แต่ง่ายกว่าที่จะคิดว่าเป็นเพียงประเภท) Maybe tซึ่งประกอบด้วยองค์ประกอบของรูปแบบJust tและNothing. เนื่องจากNothingมีประเภทMaybe tนิพจน์ที่สามารถประเมินเป็นค่าใดNothingค่าหนึ่งหรือค่าบางประเภทtจึงไม่ถูกพิมพ์อย่างถูกต้อง หากฟังก์ชันส่งคืนNothingในบางกรณีนิพจน์ใด ๆ ที่ใช้ฟังก์ชันนั้นจะต้องมีวิธีการตรวจสอบสิ่งนั้น ( isJustหรือคำสั่ง case) เพื่อจัดการกับกรณีที่เป็นไปได้ทั้งหมด
qaphla

2
ดังนั้น Just จึงมีความสอดคล้องกันภายในประเภทอาจจะเนื่องจาก t ปกติไม่ได้อยู่ในประเภทบางที ทุกอย่างชัดเจนขึ้นมากในขณะนี้ ขอบคุณ!
reem

3
@qaphla: ความคิดเห็นของคุณเกี่ยวกับการเป็น "monad จริง [... ]" นั้นทำให้เข้าใจผิด Maybe t เป็นเพียงประเภท ความจริงที่ว่ามีMonadตัวอย่างสำหรับMaybeไม่แปลงเป็นสิ่งที่ไม่ใช่ประเภท
Sarah

2

ฟังก์ชันทั้งหมด a-> b สามารถค้นหาค่าของประเภท b สำหรับค่าที่เป็นไปได้ทั้งหมดของประเภท a

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

การออกแบบอื่น ๆ ที่จัดการกับฟังก์ชันที่ไม่ใช่ทั้งหมด:

  • ข้อยกเว้นเมื่อตรวจสอบค่าอินพุตไม่พอดีกับช่วง
  • ส่งคืนค่าพิเศษ (ชนิดดั้งเดิม): ตัวเลือกที่ชื่นชอบคือค่าลบสำหรับฟังก์ชันจำนวนเต็มที่มีไว้เพื่อส่งกลับตัวเลขธรรมชาติ (ตัวอย่างเช่น String.indexOf - เมื่อไม่พบสตริงย่อยดัชนีที่ส่งคืนมักจะออกแบบให้เป็นค่าลบ)
  • ส่งคืนค่าพิเศษ (ตัวชี้): NULL หรือบางอย่าง
  • ส่งคืนโดยไม่ทำอะไรเลยตัวอย่างเช่นlendสามารถเขียนเพื่อคืนยอดเงินเก่าหากไม่เป็นไปตามเงื่อนไขในการให้ยืม
  • ส่งคืนค่าพิเศษ: ไม่มีอะไร (หรือการตัดวัตถุคำอธิบายข้อผิดพลาดบางอย่างทิ้งไว้)

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

ปัญหาในการส่งคืนค่าพิเศษหรือข้อยกเว้นการขว้างปาก็คือผู้โทรจะละเว้นการจัดการความเป็นไปได้ดังกล่าวโดยไม่ได้ตั้งใจได้ง่าย

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

วิธีการแก้ปัญหาของ Haskell บังคับให้ผู้เรียกใช้ฟังก์ชันบางส่วนจัดการกับประเภทที่ชอบMaybe aหรือEither error aเนื่องจากประเภทการส่งคืนของฟังก์ชัน

วิธีนี้lendตามที่กำหนดไว้เป็นฟังก์ชันที่ไม่ได้คำนวณยอดดุลใหม่เสมอไป - ในบางสถานการณ์ไม่ได้กำหนดยอดดุลใหม่ เราส่งสัญญาณสถานการณ์นี้ไปยังผู้โทรโดยส่งคืนค่าพิเศษ Nothing หรือโดยการรวมยอดดุลใหม่ใน Just โทรตอนนี้มีเสรีภาพในการเลือก: ทั้งจัดการกับความล้มเหลวที่จะให้ยืมในวิธีพิเศษหรือไม่สนใจและใช้ความสมดุลเก่า - maybe oldBalance id $ lend amount oldBalanceยกตัวอย่างเช่น


-1

ฟังก์ชั่นif (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)ต้องมีชนิดเดียวกันและifTrueifFalse

ดังนั้นเมื่อเราเขียนthen Nothingเราต้องใช้Maybe atype inelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type

1
ฉันแน่ใจว่าคุณตั้งใจจะพูดอะไรบางอย่างที่นี่ลึกมาก
ดู

1
Haskell มีการอนุมานประเภท ไม่จำเป็นต้องระบุประเภทNothingและJust newBalanceอย่างชัดเจน
พับขวา

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