ทำไมไม่อนุญาตให้มีการบรรทุกมากเกินไปกับประเภทการคืนสินค้า (อย่างน้อยก็ในภาษาที่ใช้โดยทั่วไป)


9

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

ฉันหมายถึงสิ่งนี้:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

ไม่ใช่ว่ามันเป็นปัญหาใหญ่สำหรับการเขียนโปรแกรม แต่บางครั้งฉันก็ยินดีต้อนรับ

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

ฉันไม่รู้ว่าคอมไพเลอร์ทำงานอย่างไร แต่มันก็ดูไม่ยากที่จะทำดังนั้นฉันจึงสงสัยว่าทำไมบางสิ่งเช่นนั้นถึงไม่ได้รับการปรับแต่ง

อะไรคือสาเหตุที่ทำให้บางอย่างเช่นนั้นไม่รองรับ


บางทีคำถามของคุณจะดีขึ้นด้วยตัวอย่างที่แปลงนัยระหว่างสองประเภทผลตอบแทนไม่อยู่ - การเช่นการเรียนและFoo Bar

ทำไมคุณสมบัติดังกล่าวถึงมีประโยชน์?
James Youngman

3
@JamesYoungman: ตัวอย่างเช่นการแยกสตริงออกเป็นประเภทต่าง ๆ คุณสามารถมีวิธีการอ่าน (String s), อ่านลอย (String s) และอื่น ๆ แต่ละรูปแบบที่มากเกินไปของวิธีการแยกวิเคราะห์สำหรับประเภทที่เหมาะสม
Giorgio

นี่เป็นเพียงปัญหาของภาษาที่พิมพ์แบบคงที่เท่านั้น การมีหลายประเภทการส่งคืนเป็นกิจวัตรประจำวันในภาษาที่พิมพ์แบบไดนามิกเช่น Javascript หรือ Python
Gort the Robot

1
@StevenBurnap อืมไม่มี ด้วยเช่นจาวาสคริปต์คุณจะไม่สามารถทำหน้าที่โอเวอร์โหลดได้เลย ดังนั้นนี่เป็นเพียงปัญหาของภาษาที่รองรับชื่อฟังก์ชั่นมากไป
David Arno

คำตอบ:


19

มันซับซ้อนการตรวจสอบประเภท

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

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

เมื่อคุณอนุญาตให้ข้อมูลประเภทเดินทางไปทั้งสองทิศทางเช่นการลดประเภทของตัวแปรจากการใช้งานคุณต้องมีตัวแก้ไขข้อ จำกัด (เช่น Algorithm W สำหรับระบบประเภท Hindley – Milner) เพื่อกำหนดประเภท

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

ที่นี่เราต้องออกจากประเภทของxตัวแปรประเภทที่ยังไม่ได้แก้ไข∃Tซึ่งสิ่งที่เรารู้เกี่ยวกับมันคือมันเป็นเรื่องที่แยกวิเคราะห์ได้ เพียง แต่ต่อมาเมื่อxมีการใช้งานในประเภทคอนกรีตทำเรามีข้อมูลเพียงพอที่จะแก้ข้อ จำกัด และตรวจสอบว่า∃T = intซึ่งแพร่กระจายข้อมูลประเภทลงxต้นไม้ไวยากรณ์จากการแสดงออกของการโทรเข้า

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

จากนี้นักออกแบบภาษาสามารถสรุป:

  • มันเพิ่มความซับซ้อนในการใช้งาน

  • มันทำให้การพิมพ์ช้าลง - ในกรณีทางพยาธิวิทยา

  • มันยากที่จะสร้างข้อความแสดงข้อผิดพลาดที่ดี

  • มันแตกต่างจากสภาพที่เป็นอยู่มากเกินไป

  • ฉันไม่รู้สึกอยากจะใช้มัน


1
นอกจากนี้: จะเป็นการยากที่จะเข้าใจว่าในบางกรณีคอมไพเลอร์ของคุณจะเลือกตัวเลือกที่โปรแกรมเมอร์ไม่ได้คาดหวังนำไปสู่การหาบั๊กยาก
gnasher729

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

4

เพราะมันคลุมเครือ ใช้ C # เป็นตัวอย่าง:

var foo = method(42);

เราควรใช้เกินพิกัดใด

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

แล้วอันนี้ละ? (ลายเซ็นที่กำหนดขึ้นใหม่เล็กน้อยเพื่อแสดงจุด)

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

เราควรใช้งานintเกินพิกัดหรือshortโอเวอร์โหลดหรือไม่? เราไม่ทราบเราจะต้องระบุด้วย[int] method1(num)ไวยากรณ์ของคุณ ซึ่งเป็นบิตของความเจ็บปวดในการแยกวิเคราะห์และเขียนถึงความซื่อสัตย์

long foo = [int] method(42);

สิ่งนี้คือมันคล้ายกับไวยากรณ์ทั่วไปใน C #

long foo = method<int>(42);

(C ++ และ Java มีคุณสมบัติที่คล้ายกัน)

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

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


จุดที่ดีเกี่ยวกับข้อมูลทั่วไป แต่คุณดูเหมือนจะเป็นมหันต์และshort int
Robert Harvey

ใช่ @RobertHarvey คุณพูดถูก ฉันพยายามยกตัวอย่างเพื่ออธิบายประเด็น มันจะทำงานได้ดีขึ้นถ้าmethodส่งคืนแบบสั้นหรือ int และประเภทนั้นถูกกำหนดเป็นความยาว
RubberDuck

ดูเหมือนว่าจะดีขึ้นเล็กน้อย
RubberDuck

ฉันไม่ได้ซื้อข้อโต้แย้งของคุณที่คุณไม่สามารถอนุมานประเภทรูทีนย่อยที่ไม่ระบุชื่อหรือความเข้าใจในภาษาที่มีการพิมพ์มากเกินไปกลับประเภท Haskell ทำและ Haskell มีทั้งหมดสามอย่าง นอกจากนี้ยังมีตัวแปรหลากหลาย จุดของคุณเกี่ยวกับlong/ int/ shortเป็นเรื่องเกี่ยวกับความซับซ้อนของการพิมพ์ย่อยและ / หรือการแปลงโดยนัยมากกว่าเกี่ยวกับการกลับมาพิมพ์มากเกินไป ท้ายที่สุดแล้วตัวอักษรจำนวนมากมีมากเกินไปในประเภทการส่งคืนใน C ++, Java, C♯และอื่น ๆ อีกมากมายและดูเหมือนจะไม่มีปัญหา คุณสามารถสร้างกฎได้ง่ายๆเช่นเลือกประเภทเฉพาะ / ทั่วไป
Jörg W Mittag

@ JörgWMittagจุดของฉันไม่ใช่ว่ามันจะทำให้มันเป็นไปไม่ได้ แต่มันทำให้สิ่งที่ซับซ้อนโดยไม่จำเป็น
RubberDuck

0

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

ในภาษาส่วนใหญ่คุณคาดหวังว่านิพจน์method1(2)จะมีชนิดที่แน่นอนและมีค่าส่งคืนที่คาดเดาได้มากขึ้นหรือน้อยลง แต่ถ้าคุณยอมให้โหลดมากเกินไปในค่าส่งคืนหมายความว่าเป็นไปไม่ได้ที่จะบอกว่านิพจน์นั้นมีความหมายโดยทั่วไปโดยไม่พิจารณาบริบทรอบ ๆ พิจารณาว่าเกิดอะไรขึ้นเมื่อคุณมีunsigned long long foo()วิธีที่การใช้งานสิ้นสุดลงreturn method1(2)? ควรเรียกการเรียกlongคืนค่าเกินพิกัดหรือเรียกintคืนค่าเกินพิกัดหรือให้ข้อผิดพลาดคอมไพเลอร์หรือไม่?

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


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


2
"พร้อมกับต้องการฟังก์ชั่นทั้งหมดให้บริสุทธิ์และโปร่งใส referential": สิ่งนี้ทำให้การคืนโหลดมากเกินไปง่ายขึ้นได้อย่างไร?
Giorgio

@Giorgio มันไม่ได้ - Rust ไม่บังคับใช้ฟังก์ชั่น puritiy และมันยังสามารถทำกลับมาพิมพ์ได้มากเกินไป (แม้ว่าการโอเวอร์โหลดของเธอใน Rust จะแตกต่างจากภาษาอื่นมาก (คุณสามารถโอเวอร์โหลดโดยใช้แม่แบบ))
Idan Arye

ส่วน [ยาว] และ [int] จะต้องมีวิธีการที่จะเรียกวิธีการอย่างชัดเจนในกรณีส่วนใหญ่ว่ามันควรจะเรียกว่าสามารถอนุมานได้โดยตรงจากประเภทของตัวแปรที่การดำเนินการของวิธีการที่ได้รับมอบหมาย
2638180

0

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

ฉันใช้สิ่งนี้ในการเข้ารหัส / ถอดรหัส APIอย่างง่าย

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

นั่นหมายความว่าการเรียกที่ประเภทพารามิเตอร์เป็นที่รู้จักเช่นinitวัตถุการทำงานง่ายมาก:

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

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


-5
int main() {
    auto var1 = method1(1);
}

ในกรณีนั้นคอมไพเลอร์สามารถ a) ปฏิเสธการโทรเพราะมันคลุมเครือ b) เลือกตัวแรก / ตัวสุดท้าย c) ออกจากประเภทvar1และดำเนินการกับการอนุมานประเภทและทันทีที่นิพจน์อื่น ๆ กำหนดประเภทของการvar1ใช้งานสำหรับการเลือก การดำเนินการที่เหมาะสม ที่บรรทัดล่างการแสดงกรณีที่การอนุมานประเภทนั้นไม่ใช่เรื่องไม่สำคัญพิสูจน์ได้ว่าเป็นจุดอื่นที่ไม่ใช่การอนุมานประเภทนั้นโดยทั่วไปจะไม่น่ารำคาญ
back2dos

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

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