Mengapa kelebihan beban dengan jenis pengembalian tidak diizinkan? (setidaknya dalam bahasa yang biasanya digunakan)

9

Saya tidak tahu tentang semua bahasa pemrograman, tetapi jelas bahwa biasanya kemungkinan overloading metode dengan mempertimbangkan jenis kembalinya (dengan asumsi argumennya adalah jumlah dan jenis yang sama) tidak didukung.

Maksud saya sesuatu seperti ini:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

Ini bukan masalah besar untuk pemrograman tetapi pada beberapa kesempatan saya akan menyambutnya.

Jelas tidak akan ada cara bagi bahasa-bahasa tersebut untuk mendukung itu tanpa ada cara untuk membedakan metode apa yang sedang dipanggil, tetapi sintaks untuk itu dapat sesederhana sesuatu seperti [int] method1 (num) atau [long] method1 (num) dengan cara itu kompiler akan tahu mana yang akan dipanggil.

Saya tidak tahu tentang bagaimana kompiler bekerja tetapi itu tampaknya tidak terlalu sulit untuk dilakukan, jadi saya bertanya-tanya mengapa hal seperti itu biasanya tidak dilaksanakan.

Apa alasan mengapa hal seperti itu tidak didukung?

pengguna2638180
sumber
Mungkin pertanyaan Anda akan lebih baik dengan contoh di mana konversi tersirat antara kedua jenis pengembalian tidak ada - misalnya kelas Foodan Bar.
Nah, mengapa fitur seperti itu bermanfaat?
James Youngman
3
@JamesYoungman: Sebagai contoh untuk mem-parsing string menjadi tipe yang berbeda, Anda dapat memiliki metode int read (String s), float read (String s), dan sebagainya. Setiap variasi metode yang kelebihan beban melakukan penguraian untuk jenis yang sesuai.
Giorgio
Omong-omong, ini hanya masalah bahasa yang diketik secara statis. Memiliki beberapa jenis pengembalian cukup rutin dalam bahasa yang diketik secara dinamis seperti Javascript atau Python.
Gort the Robot
1
@ SevenBurnap, um, tidak. Dengan misalnya JavaScript, Anda tidak dapat melakukan fungsi kelebihan beban sama sekali. Jadi ini sebenarnya hanya masalah dengan bahasa yang mendukung overloading nama fungsi.
David Arno

Jawaban:

19

Ini menyulitkan pengecekan tipe.

Ketika Anda hanya mengizinkan overloading berdasarkan tipe argumen, dan hanya mengizinkan deduksi tipe variabel dari inisialisasi mereka, semua informasi tipe Anda mengalir dalam satu arah: naik pohon sintaks.

var x = f();

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

Saat Anda mengizinkan informasi jenis untuk bepergian di kedua arah, seperti menyimpulkan tipe variabel dari penggunaannya, Anda memerlukan pemecah kendala (seperti Algoritma W, untuk sistem tipe Hindley-Milner) untuk menentukan jenisnya.

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]

Di sini, kita perlu meninggalkan tipe xsebagai variabel tipe yang tidak terselesaikan ∃T, di mana yang kita tahu adalah bahwa itu dapat diuraikan. Hanya nanti, ketika xdigunakan pada tipe konkret, kita memiliki cukup informasi untuk menyelesaikan kendala dan menentukan itu ∃T = int, yang menyebarkan informasi tipe ke bawah pohon sintaks dari ekspresi panggilan ke x.

Jika kami gagal menentukan jenisnya x, kode ini akan kelebihan beban juga (jadi pemanggil akan menentukan jenisnya) atau kami harus melaporkan kesalahan tentang ambiguitas.

Dari sini, seorang perancang bahasa dapat menyimpulkan:

  • Ini menambah kompleksitas implementasi.

  • Itu membuat pemeriksaan ketik lebih lambat — dalam kasus patologis, secara eksponensial terjadi.

  • Lebih sulit menghasilkan pesan kesalahan yang baik.

  • Itu terlalu berbeda dari status quo.

  • Saya tidak merasa ingin mengimplementasikannya.

Jon Purdy
sumber
1
Juga: Akan sangat sulit untuk memahami bahwa dalam beberapa kasus, kompiler Anda akan membuat pilihan yang sama sekali tidak diharapkan oleh programmer, sehingga sulit menemukan bug.
gnasher729
1
@ gnasher729: Saya tidak setuju. Saya secara teratur menggunakan Haskell, yang memiliki fitur ini, dan saya tidak pernah digigit oleh pilihannya yang berlebihan (yaitu contoh typeclass). Jika ada sesuatu yang ambigu, itu hanya memaksa saya untuk menambahkan anotasi jenis. Dan saya masih memilih untuk menerapkan inferensi tipe penuh dalam bahasa saya karena ini sangat berguna. Jawaban ini adalah saya berperan sebagai penasihat iblis.
Jon Purdy
4

Karena ambigu. Menggunakan C # sebagai contoh:

var foo = method(42);

Kelebihan mana yang harus kita gunakan?

Ok, mungkin itu agak tidak adil. Kompiler tidak dapat mengetahui jenis apa yang akan digunakan jika kami tidak memberitahukannya dalam bahasa hipotetis Anda. Jadi, pengetikan tersirat tidak mungkin dalam bahasa Anda dan ada metode anonim dan Linq bersama dengannya ...

Bagaimana dengan yang ini? (Tanda tangan sedikit didefinisikan ulang untuk menggambarkan titik.)

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

....

long foo = method(42);

Haruskah kita menggunakan intkelebihan, atau shortkelebihan? Kami tidak tahu, kami harus menentukannya dengan [int] method1(num)sintaks Anda . Yang agak menyakitkan untuk diurai dan menulis jujur.

long foo = [int] method(42);

Masalahnya, sintaksisnya mirip dengan metode generik dalam C #.

long foo = method<int>(42);

(C ++ dan Java memiliki fitur serupa.)

Singkatnya, perancang bahasa memilih untuk memecahkan masalah dengan cara yang berbeda untuk menyederhanakan penguraian dan mengaktifkan fitur bahasa yang jauh lebih kuat.

Anda mengatakan Anda tidak tahu banyak tentang kompiler. Saya sangat merekomendasikan belajar tentang tata bahasa dan parser. Setelah Anda memahami apa itu tata bahasa bebas konteks, Anda akan memiliki ide yang lebih baik tentang mengapa ambiguitas adalah hal yang buruk.

Bebek karet
sumber
Poin bagus tentang obat generik, meskipun Anda tampaknya menyatu shortdan int.
Robert Harvey
Ya @RobertHarvey kau benar. Saya sedang berjuang untuk sebuah contoh untuk menggambarkan hal tersebut. Ini akan bekerja lebih baik jika methodmengembalikan pendek atau int, dan jenisnya didefinisikan sebagai panjang.
RubberDuck
Tampaknya sedikit lebih baik.
RubberDuck
Saya tidak membeli argumen Anda bahwa Anda tidak dapat memiliki jenis inferensi, subrutin anonim, atau pemahaman monad dalam bahasa dengan tipe pengembalian berlebih. Haskell melakukannya, dan Haskell memiliki ketiganya. Ini juga memiliki polimorfisme parametrik. Poin Anda tentang long/ int/ shortlebih tentang kompleksitas subtyping dan / atau konversi implisit daripada tentang kelebihan tipe pengembalian. Setelah semua, jumlah literal yang kelebihan beban pada jenis mereka kembali di C ++, Java, C♯, dan banyak lainnya, dan bahwa tampaknya tidak ada masalah. Anda dapat membuat aturan: misalnya memilih jenis yang paling spesifik / umum.
Jörg W Mittag
@ JörgWMittag maksud saya bukan bahwa itu akan membuatnya tidak mungkin, hanya saja itu membuat hal-hal yang tidak perlu menjadi rumit.
RubberDuck
0

Semua fitur bahasa menambah kompleksitas, sehingga mereka harus memberikan manfaat yang cukup untuk membenarkan gotcha yang tak terhindarkan, kasing sudut, dan kebingungan pengguna yang dibuat oleh setiap fitur. Untuk sebagian besar bahasa, yang satu ini tidak memberikan manfaat yang cukup untuk membenarkannya.

Dalam sebagian besar bahasa, Anda akan mengharapkan ekspresi method1(2)memiliki tipe yang pasti dan nilai pengembalian yang lebih atau kurang dapat diprediksi. Tetapi jika Anda mengizinkan overloading pada nilai yang dikembalikan, itu berarti tidak mungkin untuk mengatakan apa arti ekspresi itu secara umum tanpa mempertimbangkan konteks di sekitarnya. Pertimbangkan apa yang terjadi ketika Anda memiliki unsigned long long foo()metode yang implementasinya berakhir return method1(2)? Haruskah yang memanggil long-returning overload atau int-returning overload atau hanya memberikan kesalahan kompilator?

Plus, jika Anda harus membantu kompiler dengan membubuhi keterangan tipe pengembalian, Anda tidak hanya menciptakan lebih banyak sintaks (yang meningkatkan semua biaya yang disebutkan untuk memungkinkan fitur ada sama sekali), tetapi Anda secara efektif melakukan hal yang sama seperti membuat dua metode dengan nama berbeda dalam bahasa "normal". Apakah [long] method1(2)lebih intuitif daripada long_method1(2)?


Di sisi lain, beberapa bahasa fungsional seperti Haskell dengan sistem tipe statis yang sangat kuat memungkinkan perilaku semacam ini, karena inferensi tipe mereka cukup kuat sehingga Anda jarang perlu membuat anotasi jenis pengembalian dalam bahasa tersebut. Tapi itu hanya mungkin karena bahasa-bahasa itu benar-benar menegakkan keamanan jenis, lebih dari bahasa tradisional mana pun, bersama dengan mengharuskan semua fungsi menjadi murni dan transparan referensial. Itu bukan sesuatu yang akan layak di sebagian besar bahasa OOP.

Ixrec
sumber
2
"bersama dengan mengharuskan semua fungsi menjadi murni dan transparan referensial": Bagaimana ini membuat kelebihan tipe pengembalian lebih mudah?
Giorgio
@Giorgio Tidak - Rust tidak memberlakukan puritiy fungsi dan masih dapat melakukan overloading tipe kembali (meskipun overloadingnya di Rust sangat berbeda dari bahasa lain (Anda hanya dapat membebani dengan menggunakan templat))
Idan Arye
Bagian [long] dan [int] adalah memiliki cara untuk secara eksplisit memanggil metode, pada kebanyakan kasus bagaimana seharusnya dipanggil dapat secara langsung disimpulkan dari jenis variabel yang pelaksanaannya ditugaskan untuk metode.
user2638180
0

Hal ini tersedia dalam Swift dan bekerja dengan baik di sana. Jelas Anda tidak dapat memiliki tipe ambigu di kedua sisi sehingga harus diketahui di sebelah kiri.

Saya menggunakan ini dalam penyandian sederhana / decode API .

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

Itu berarti panggilan di mana tipe parameter diketahui, seperti initobjek, bekerja sangat sederhana:

    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)
    }

Jika Anda memperhatikan dengan seksama, Anda akan melihat dechOptpanggilan di atas. Saya menemukan cara sulit bahwa membebani nama fungsi yang sama di mana pembeda mengembalikan opsi terlalu rentan terhadap kesalahan karena konteks panggilan dapat memperkenalkan harapan itu adalah opsional.

Andy Dent
sumber
-5
int main() {
    auto var1 = method1(1);
}
DeadMG
sumber
Dalam hal ini kompiler dapat a) menolak panggilan karena ambigu b) memilih yang pertama / terakhir c) meninggalkan tipe var1dan melanjutkan dengan tipe inferensi dan segera setelah beberapa ekspresi lain menentukan jenis var1penggunaan yang untuk memilih implementasi yang tepat. Pada intinya, menunjukkan kasus di mana inferensi tipe non-sepele jarang membuktikan titik selain inferensi tipe umumnya non-sepele.
back2dos
1
Bukan argumen yang kuat. Karat, misalnya, banyak menggunakan inferensi jenis, dan dalam beberapa kasus, terutama dengan obat generik, tidak dapat menentukan jenis apa yang Anda butuhkan. Dalam kasus seperti itu, Anda hanya perlu memberikan tipe secara eksplisit daripada mengandalkan inferensi tipe.
8bittree
1
Uh ... itu tidak menunjukkan tipe pengembalian yang kelebihan beban. method1harus dinyatakan untuk mengembalikan jenis tertentu.
Gort the Robot