Bertentangan dengan apa yang orang lain katakan, overloading oleh jenis kembali adalah mungkin dan ini dilakukan oleh beberapa bahasa modern. Keberatan yang biasa adalah bahwa dalam kode suka
int func();
string func();
int main() { func(); }
Anda tidak tahu mana func()
yang dipanggil. Ini dapat diatasi dengan beberapa cara:
- Memiliki metode yang dapat diprediksi untuk menentukan fungsi mana yang dipanggil dalam situasi seperti itu.
- Setiap kali situasi seperti itu terjadi, itu adalah kesalahan waktu kompilasi. Namun, memiliki sintaks yang memungkinkan programmer untuk ambigu, misalnya
int main() { (string)func(); }
.
- Tidak memiliki efek samping. Jika Anda tidak memiliki efek samping dan Anda tidak pernah menggunakan nilai pengembalian suatu fungsi, maka kompiler dapat menghindari pemanggilan fungsi di tempat pertama.
Dua dari bahasa yang saya gunakan ( ab ) secara teratur menggunakan tipe overload: Perl dan Haskell . Biarkan saya jelaskan apa yang mereka lakukan.
Dalam Perl , ada perbedaan mendasar antara skalar dan konteks daftar (dan yang lainnya, tapi kami akan berpura-pura ada dua). Setiap fungsi bawaan di Perl dapat melakukan hal-hal yang berbeda tergantung pada konteksnya . Misalnya, join
operator memaksa konteks daftar (pada hal yang sedang digabungkan) sementara scalar
operator memaksa konteks skalar, jadi bandingkan:
print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.
Setiap operator di Perl melakukan sesuatu dalam konteks skalar dan sesuatu dalam konteks daftar, dan mereka mungkin berbeda, seperti yang diilustrasikan. (Ini bukan hanya untuk operator acak seperti localtime
. Jika Anda menggunakan array @a
dalam konteks daftar, itu akan mengembalikan array, sedangkan dalam konteks skalar, ia mengembalikan jumlah elemen. Jadi misalnya print @a
mencetak elemen, sambil print 0+@a
mencetak ukuran. ) Selanjutnya, setiap operator dapat memaksakan suatu konteks, misalnya penambahan +
kekuatan konteks skalar. Setiap entri dalam man perlfunc
dokumen ini. Misalnya, berikut adalah bagian dari entri untuk glob EXPR
:
Dalam konteks daftar, mengembalikan daftar (mungkin kosong) ekspansi nama file pada nilai EXPR
seperti yang /bin/csh
akan dilakukan shell Unix standar . Dalam konteks skalar, glob iterates melalui ekspansi nama file seperti itu, mengembalikan undef ketika daftar habis.
Sekarang, apa hubungan antara daftar dan konteks skalar? Baiklah, man perlfunc
kata
Ingat aturan penting berikut: Tidak ada aturan yang menghubungkan perilaku ekspresi dalam konteks daftar dengan perilaku dalam konteks skalar, atau sebaliknya. Mungkin melakukan dua hal yang sama sekali berbeda. Setiap operator dan fungsi memutuskan nilai seperti apa yang paling tepat untuk dikembalikan dalam konteks skalar. Beberapa operator mengembalikan panjang daftar yang akan dikembalikan dalam konteks daftar. Beberapa operator mengembalikan nilai pertama dalam daftar. Beberapa operator mengembalikan nilai terakhir dalam daftar. Beberapa operator mengembalikan hitungan operasi yang berhasil. Secara umum, mereka melakukan apa yang Anda inginkan, kecuali jika Anda menginginkan konsistensi.
jadi itu bukan masalah sederhana memiliki fungsi tunggal, dan kemudian Anda melakukan konversi sederhana pada akhirnya. Sebenarnya, saya memilih localtime
contoh karena alasan itu.
Bukan hanya built-in yang memiliki perilaku ini. Setiap pengguna dapat mendefinisikan fungsi tersebut menggunakan wantarray
, yang memungkinkan Anda untuk membedakan antara daftar, skalar, dan konteks batal. Jadi, misalnya, Anda dapat memutuskan untuk tidak melakukan apa pun jika Anda dipanggil dalam konteks kosong.
Sekarang, Anda mungkin mengeluh bahwa ini bukan kelebihan yang sebenarnya dengan nilai pengembalian karena Anda hanya memiliki satu fungsi, yang diberi tahu konteks tempat ia dipanggil dan kemudian bertindak berdasarkan informasi itu. Namun, ini jelas setara (dan analog dengan bagaimana Perl tidak mengizinkan overloading biasa secara harfiah, tetapi suatu fungsi hanya dapat memeriksa argumennya). Selain itu, ini dengan baik menyelesaikan situasi ambigu yang disebutkan di awal respons ini. Perl tidak mengeluh bahwa ia tidak tahu metode panggilan mana; itu hanya menyebutnya. Yang harus dilakukan adalah mencari tahu konteks apa fungsi dipanggil, yang selalu mungkin:
sub func {
if( not defined wantarray ) {
print "void\n";
} elsif( wantarray ) {
print "list\n";
} else {
print "scalar\n";
}
}
func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"
(Catatan: Saya kadang-kadang mengatakan operator Perl ketika maksud saya berfungsi. Ini tidak penting untuk diskusi ini.)
Haskell mengambil pendekatan lain, yaitu tidak memiliki efek samping. Ini juga memiliki sistem tipe yang kuat, sehingga Anda dapat menulis kode seperti berikut:
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
Kode ini membaca angka floating point dari input standar, dan mencetak root kuadratnya. Tapi apa yang mengejutkan tentang ini? Nah, jenisnya readLn
adalah readLn :: Read a => IO a
. Ini artinya bahwa untuk semua jenis yang dapat Read
(secara formal, setiap jenis yang merupakan turunan dari Read
kelas jenis), readLn
dapat membacanya. Bagaimana Haskell tahu bahwa saya ingin membaca angka floating point? Ya, tipe sqrt
is sqrt :: Floating a => a -> a
, yang pada dasarnya berarti sqrt
hanya dapat menerima angka floating point sebagai input, dan Haskell menyimpulkan apa yang saya inginkan.
Apa yang terjadi ketika Haskell tidak dapat menyimpulkan apa yang saya inginkan? Nah, ada beberapa kemungkinan. Jika saya tidak menggunakan nilai kembali sama sekali, Haskell tidak akan memanggil fungsi di tempat pertama. Namun, jika saya lakukan menggunakan nilai kembali, maka Haskell akan mengeluh bahwa ia tidak dapat menyimpulkan jenis:
main = do n <- readLn
print n
-- this program results in a compile-time error "Unresolved top-level overloading"
Saya dapat mengatasi ambiguitas dengan menentukan jenis yang saya inginkan:
main = do n <- readLn
print (n::Int)
-- this compiles (and does what I want)
Bagaimanapun, apa arti seluruh diskusi ini adalah bahwa kelebihan dengan nilai pengembalian adalah mungkin dan dilakukan, yang menjawab bagian dari pertanyaan Anda.
Bagian lain dari pertanyaan Anda adalah mengapa lebih banyak bahasa tidak melakukannya. Saya akan membiarkan orang lain menjawab itu. Namun, beberapa komentar: alasan prinsipnya mungkin adalah bahwa kesempatan untuk kebingungan benar-benar lebih besar di sini daripada kelebihan beban berdasarkan tipe argumen. Anda juga dapat melihat alasan dari masing-masing bahasa:
Ada : "Mungkin terlihat bahwa aturan resolusi kelebihan beban paling sederhana adalah menggunakan segala sesuatu - semua informasi dari konteks seluas mungkin - untuk menyelesaikan referensi kelebihan beban. Aturan ini mungkin sederhana, tetapi tidak membantu. Ini membutuhkan pembaca manusia untuk memindai teks berukuran besar secara sewenang-wenang, dan untuk membuat kesimpulan kompleks yang sewenang-wenang (seperti (g) di atas). Kami percaya bahwa aturan yang lebih baik adalah aturan yang membuat eksplisit tugas yang harus dilakukan oleh pembaca manusia atau kompiler, dan yang membuat tugas ini sealami mungkin bagi pembaca manusia. "
C ++ (subbab 7.4.1 dari Bjarne Stroustrup "The C ++ Programming Language"): "Jenis kembali tidak dianggap dalam resolusi kelebihan beban. Alasannya adalah untuk menjaga resolusi untuk operator individu atau fungsi panggilan konteks-independen. Pertimbangkan:
float sqrt(float);
double sqrt(double);
void f(double da, float fla)
{
float fl = sqrt(da); // call sqrt(double)
double d = sqrt(da); // call sqrt(double)
fl = sqrt(fla); // call sqrt(float)
d = sqrt(fla); // call sqrt(float)
}
Jika jenis pengembalian diperhitungkan, tidak mungkin lagi melihat panggilan sqrt()
secara terpisah dan menentukan fungsi mana yang dipanggil. "(Perhatikan, sebagai perbandingan, bahwa di Haskell tidak ada konversi implisit .)
Java ( Spesifikasi Bahasa Jawa 9.4.1 ): "Salah satu metode yang diwariskan harus dapat diganti-ketik-kembali untuk setiap metode warisan lainnya, atau kesalahan kompilasi waktu terjadi." (Ya, saya tahu ini tidak memberikan alasan. Saya yakin alasannya diberikan oleh Gosling dalam "Bahasa Pemrograman Java". Mungkin seseorang memiliki salinannya? Saya yakin itu adalah "prinsip kejutan paling tidak" pada dasarnya. ) Namun, fakta menarik tentang Java: JVM memungkinkan kelebihan dengan nilai kembali! Ini digunakan, misalnya, dalam Scala , dan dapat diakses langsung melalui Java juga dengan bermain-main dengan internal.
PS. Sebagai catatan akhir, sebenarnya dimungkinkan untuk membebani dengan mengembalikan nilai dalam C ++ dengan sebuah trik. Saksi:
struct func {
operator string() { return "1";}
operator int() { return 2; }
};
int main( ) {
int x = func(); // calls int version
string y = func(); // calls string version
double d = func(); // calls int version
cout << func() << endl; // calls int version
func(); // calls neither
}