การจับคู่รูปแบบกับประเภทการออกแบบที่ใช้สำนวนหรือไม่ดีหรือไม่?


18

ดูเหมือนว่ารหัส F # มักจะจับคู่รูปแบบกับประเภท อย่างแน่นอน

match opt with 
| Some val -> Something(val) 
| None -> Different()

ดูเหมือนทั่วไป

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

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

นี่คือรหัสเพิ่มเติมอย่างแน่นอน OTOH ดูเหมือนว่า OOP-y ในใจของฉันจะมีข้อได้เปรียบทางโครงสร้าง:

  • การขยายสู่รูปแบบใหม่Tนั้นง่าย
  • ฉันไม่ต้องกังวลกับการค้นหาเส้นทางการควบคุมการเลือกเส้นทางซ้ำซ้อน และ
  • การเลือกเส้นทางนั้นไม่เปลี่ยนแปลงในแง่ที่ว่าเมื่อฉันมีFooในมือฉันไม่จำเป็นต้องกังวลเกี่ยวกับBar.Route()การนำไปใช้

มีข้อได้เปรียบในการจับคู่รูปแบบกับประเภทที่ฉันไม่เห็นหรือไม่ มันถือว่าเป็นสำนวนหรือเป็นความสามารถที่ไม่ได้ใช้กันทั่วไปหรือไม่?


4
การรับรู้ภาษาที่ใช้งานได้จากมุมมองของ OOP มีความหมายมากเพียงใด อย่างไรก็ตามพลังที่แท้จริงของการจับคู่รูปแบบมาพร้อมกับรูปแบบที่ซ้อนกัน เพียงแค่ตรวจสอบตัวสร้างชั้นนอกสุดที่เป็นไปได้ แต่ไม่ได้หมายความว่าเรื่องราวทั้งหมด
Ingo

นี่ - But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.- ฟังไม่เชื่อฟังเกินไป บางครั้งคุณต้องการแยก ops ของคุณออกจากลำดับชั้น: บางที 1) คุณไม่สามารถเพิ่ม op ในลำดับชั้น b / c ที่คุณไม่ได้เป็นเจ้าของลำดับชั้น 2) คลาสที่คุณต้องการให้ op ไม่ตรงกับลำดับชั้นของคุณ 3) คุณสามารถเพิ่ม op ให้กับลำดับชั้นของคุณ แต่ไม่ต้องการ b / c คุณไม่ต้องการถ่วง API ของลำดับชั้นของคุณด้วยอึมากมายที่ลูกค้าส่วนใหญ่ไม่ได้ใช้

4
เพียงชี้แจงSomeและNoneไม่ใช่ประเภท พวกเขาเป็นทั้งคอนสตรัคเตอร์ที่มีประเภทforall a. a -> option aและforall a. option a(ขออภัยไม่แน่ใจว่าไวยากรณ์สำหรับคำอธิบายประกอบประเภทคืออะไรใน F #)

คำตอบ:


21

คุณถูกต้องในลำดับชั้นของคลาส OOP นั้นมีความสัมพันธ์อย่างใกล้ชิดกับสหภาพที่ถูกเลือกปฏิบัติใน F # และการจับคู่รูปแบบนั้นมีความสัมพันธ์อย่างใกล้ชิดกับการทดสอบประเภทไดนามิก อันที่จริงแล้วนี่เป็นวิธีที่ F # รวบรวมสหภาพที่แบ่งแยกโดยเลือกปฏิบัติเป็น. NET!

เกี่ยวกับความสามารถในการขยายมีสองด้านของปัญหา:

  • OO ให้คุณเพิ่มคลาสย่อยใหม่ แต่ทำให้ยากที่จะเพิ่มฟังก์ชั่นใหม่ (เสมือน)
  • FP ช่วยให้คุณเพิ่มฟังก์ชั่นใหม่ แต่ทำให้ยากในการเพิ่มเคสแบบใหม่

ที่กล่าวว่า F # จะให้คำเตือนเมื่อคุณพลาดเคสในการจับคู่รูปแบบดังนั้นการเพิ่มเคสใหม่จะไม่เลว

เกี่ยวกับการค้นหาการทำซ้ำในการเลือกราก - F # จะให้คำเตือนเมื่อคุณมีการแข่งขันที่ซ้ำกันเช่น:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

ความจริงที่ว่า "ตัวเลือกเส้นทางนั้นไม่เปลี่ยนรูป" อาจเป็นปัญหาได้เช่นกัน ตัวอย่างเช่นหากคุณต้องการแบ่งปันการใช้งานฟังก์ชั่นระหว่างFooและBarกรณี แต่ทำอย่างอื่นสำหรับZooกรณีคุณสามารถเข้ารหัสที่ใช้รูปแบบการจับคู่ได้อย่างง่ายดาย:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

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

ทั้งสองวิธี - OO และ FP ค่อนข้างสมบูรณ์และทั้งคู่มีข้อดีและข้อเสีย สิ่งที่ยุ่งยาก (มาจากมุมมองของ OO) คือ F # มักจะใช้สไตล์ FP เป็นค่าเริ่มต้น แต่ถ้ามีความต้องการเพิ่มคลาสย่อยใหม่คุณสามารถใช้อินเตอร์เฟสได้เสมอ แต่ในระบบส่วนใหญ่คุณจำเป็นต้องเพิ่มประเภทและฟังก์ชั่นอย่างเท่าเทียมกันดังนั้นตัวเลือกไม่สำคัญมากนัก - และการใช้สหภาพที่เลือกปฏิบัติใน F # คือ nicer

ฉันขอแนะนำชุดบล็อกที่ยอดเยี่ยมสำหรับข้อมูลเพิ่มเติม


3
แม้ว่าคุณจะถูกต้องฉันต้องการเพิ่มว่านี่ไม่ใช่ปัญหาของ OO vs FP มากนักเนื่องจากเป็นปัญหาของออบเจ็กต์และประเภทผลรวม ความหลงไหลของ OOP กับพวกมันต่างหากไม่มีอะไรที่เกี่ยวกับวัตถุที่ทำให้พวกมันใช้งานไม่ได้ และถ้าคุณกระโดดผ่านห่วงมากพอคุณสามารถใช้ประเภทผลรวมในภาษา OOP หลักได้เช่นกัน (แม้ว่ามันจะไม่สวย)
Doval

1
"และถ้าคุณกระโดดผ่านห่วงมากพอคุณสามารถใช้ประเภทผลรวมในภาษา OOP หลัก ๆ ได้ด้วย (แม้ว่ามันจะไม่สวย) -> ผมคิดว่าคุณจะจบลงด้วยสิ่งที่คล้ายกับวิธีการ F # ประเภทผลรวมจะถูกเข้ารหัสในระบบการพิมพ์ของ .NET :)
Tarmil

8

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

ฉันอาจใช้ระบบชนิดเพื่อกำหนดประเภทที่สามารถมีชนิดย่อยจำนวนคงที่เท่านั้น:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

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

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

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


"ซึ่งหมายความว่ารูปแบบการจับคู่เป็นเหมือน downcast พิเศษที่บังคับให้เราจัดการกับตัวเลือกทั้งหมด" - ในความเป็นจริงฉันเชื่อว่า (ตราบใดที่คุณจับคู่กับตัวสร้างเท่านั้นไม่ใช่ค่าหรือโครงสร้างซ้อนกัน) มัน isomorphic วางวิธีเสมือนนามธรรมในซูเปอร์คลาส
จูลส์

2

โดยทั่วไปการจับคู่รูปแบบ F # มักจะทำกับสหภาพที่มีการแบ่งแยกมากกว่าคลาส (ซึ่งไม่ใช่เทคนิคการตรวจสอบประเภทเลย สิ่งนี้อนุญาตให้คอมไพเลอร์ให้คำเตือนเมื่อคุณไม่ได้นับกรณีสำหรับการจับคู่รูปแบบ

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

เพิ่มตัวเลือกใหม่แล้วดูเหมือนว่า:

  1. เพิ่มตัวเลือกใหม่ให้กับสหภาพที่เลือกปฏิบัติของคุณ
  2. แก้ไขคำเตือนทั้งหมดในรูปแบบที่ไม่ตรงกัน

2

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

if (opt != null)
    opt.Something()
else
    Different()

มันค่อนข้างยากที่จะใช้ polymorphism เพื่อหลีกเลี่ยงสิ่งที่ทำเป็นประจำเช่นการตรวจสอบ null ใน OOP เช่นเดียวกับโปรแกรมเมอร์ OO ไม่ได้สร้างวัตถุเป็นโมฆะในทุก ๆ สถานการณ์โปรแกรมเมอร์ที่ทำงานไม่ได้ทำงานมากเกินไปโดยเฉพาะเมื่อคุณรู้ว่ารายการรูปแบบของคุณรับประกันว่าจะครบถ้วนสมบูรณ์ หากคุณใช้ระบบพิมพ์ในสถานการณ์อื่น ๆ คุณจะเห็นว่ามันใช้ในแบบที่คุณไม่คุ้นเคย

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

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

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


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