Mengapa inferensi jenis bermanfaat?

37

Saya membaca kode jauh lebih sering daripada saya menulis kode, dan saya berasumsi bahwa sebagian besar programmer yang mengerjakan perangkat lunak industri melakukan ini. Keuntungan dari inferensi tipe yang saya asumsikan adalah kurang verbositas dan kode yang kurang tertulis. Tetapi di sisi lain, jika Anda lebih sering membaca kode, Anda mungkin menginginkan kode yang bisa dibaca.

Compiler menyimpulkan tipe; ada algoritma lama untuk ini. Tetapi pertanyaan sebenarnya adalah mengapa saya, programmer, ingin menyimpulkan jenis variabel saya ketika saya membaca kode? Bukankah lebih cepat bagi siapa pun untuk hanya membaca jenis daripada memikirkan jenis apa yang ada?

Sunting: Sebagai kesimpulan saya mengerti mengapa ini berguna. Namun dalam kategori fitur bahasa, saya melihatnya di ember dengan kelebihan operator - berguna dalam beberapa kasus tetapi memengaruhi keterbacaan jika disalahgunakan.

m3th0dman
sumber
5
Dalam pengalaman saya, jenis jauh lebih penting ketika menulis kode daripada membacanya. Saat membaca kode, saya mencari algoritme dan blok tertentu yang biasanya dinamai dengan variabel yang mengarahkan saya. Saya benar-benar tidak perlu mengetikkan kode cek hanya untuk membaca dan memahami apa yang dilakukannya kecuali sangat buruk ditulis. Namun, ketika membaca kode yang membengkak dengan detail ekstra yang tidak perlu yang tidak saya cari (seperti terlalu banyak jenis anotasi), sering kali lebih sulit untuk menemukan bit yang saya cari. Ketik inferensi yang akan saya katakan adalah keuntungan besar untuk membaca jauh lebih banyak daripada menulis kode.
Jimmy Hoffa
Setelah saya menemukan potongan kode yang saya cari, maka saya mungkin mulai mengetik memeriksanya tetapi pada waktu tertentu Anda tidak boleh fokus pada lebih dari 10 baris kode, pada titik mana tidak repot untuk melakukan inferensi diri Anda karena Anda secara mental memilih seluruh blok terpisah untuk memulai, dan kemungkinan menggunakan alat untuk membantu Anda tetap melakukannya. Mencari tahu jenis 10 baris kode yang Anda coba zona jarang jarang memakan waktu Anda sama sekali, tapi ini adalah bagian di mana Anda sudah beralih dari membaca ke menulis yang jauh lebih jarang.
Jimmy Hoffa
Ingatlah bahwa walaupun seorang programmer membaca kode lebih sering daripada menulisnya, itu tidak berarti bahwa sepotong kode lebih sering dibaca daripada ditulis. Banyak kode mungkin berumur pendek atau tidak pernah dibaca lagi, dan mungkin tidak mudah untuk mengetahui kode mana yang akan bertahan dan harus ditulis agar dapat dibaca maksimal.
jpa
2
Menguraikan poin pertama @JimmyHoffa, pertimbangkan untuk membaca secara umum. Apakah kalimat lebih mudah diurai dan dibaca, apalagi dipahami, ketika berfokus pada bagian dari kata-kata individualnya? "The (artikel) cow (noun-singular) melompati (kata kerja-past) over (preposisi) the (article) moon (noun). (Punctuation-period)".
Zev Spitz

Jawaban:

46

Mari kita lihat Java. Java tidak dapat memiliki variabel dengan tipe yang disimpulkan. Ini berarti saya sering harus mengeja jenisnya, meskipun sangat jelas bagi pembaca manusia apa jenisnya:

int x = 42;  // yes I see it's an int, because it's a bloody integer literal!

// Why the hell do I have to spell the name twice?
SomeObjectFactory<OtherObject> obj = new SomeObjectFactory<>();

Dan terkadang menjengkelkan untuk mengeja seluruh tipe.

// this code walks through all entries in an "(int, int) -> SomeObject" table
// represented as two nested maps
// Why are there more types than actual code?
for (Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>> row : table.entrySet()) {
    Integer rowKey = entry.getKey();
    Map<Integer, SomeObject<SomeObject, T>> rowValue = entry.getValue();
    for (Map.Entry<Integer, SomeObject<SomeObject, T>> col : rowValue.entrySet()) {
        Integer colKey = col.getKey();
        SomeObject<SomeObject, T> colValue = col.getValue();
        doSomethingWith<SomeObject<SomeObject, T>>(rowKey, colKey, colValue);
    }
}

Pengetikan statis verbose ini menghalangi saya, sang programmer. Sebagian besar jenis anotasi adalah pengisi baris berulang, regurgiasi bebas konten dari apa yang sudah kita ketahui. Namun, saya suka mengetik statis, karena ini sangat membantu menemukan bug, jadi menggunakan pengetikan dinamis tidak selalu merupakan jawaban yang baik. Jenis inferensi adalah yang terbaik dari kedua dunia: Saya dapat menghilangkan jenis yang tidak relevan, tetapi masih yakin bahwa program saya (tipe-) memeriksa.

Meskipun inferensi tipe sangat berguna untuk variabel lokal, itu tidak boleh digunakan untuk API publik yang harus didokumentasikan dengan jelas. Dan kadang-kadang jenisnya sangat penting untuk memahami apa yang terjadi dalam kode. Dalam kasus seperti itu, akan bodoh jika mengandalkan inferensi tipe saja.

Ada banyak bahasa yang mendukung inferensi tipe. Sebagai contoh:

  • C ++. Kata autokunci memicu inferensi jenis. Tanpa itu, mengeja jenis untuk lambdas atau untuk entri dalam wadah akan menjadi neraka.

  • C #. Anda dapat mendeklarasikan variabel dengan var, yang memicu bentuk inferensi tipe terbatas. Itu masih mengelola sebagian besar kasus di mana Anda ingin mengetik inferensi. Di tempat-tempat tertentu Anda dapat meninggalkan jenis sepenuhnya (misalnya di lambdas).

  • Haskell, dan bahasa apa pun dalam keluarga ML. Sementara rasa spesifik dari inferensi tipe yang digunakan di sini cukup kuat, Anda masih sering melihat anotasi tipe untuk fungsi, dan karena dua alasan: Yang pertama adalah dokumentasi, dan yang kedua adalah pemeriksaan bahwa inferensi tipe benar-benar menemukan tipe yang Anda harapkan. Jika ada perbedaan, kemungkinan ada beberapa jenis bug.

amon
sumber
13
Perhatikan juga bahwa C # memiliki tipe anonim, yaitu tipe tanpa nama, tetapi C # memiliki sistem tipe nominal, yaitu sistem tipe berdasarkan nama. Tanpa inferensi tipe, tipe-tipe itu tidak akan pernah bisa digunakan!
Jörg W Mittag
10
Beberapa contoh agak dibuat-buat, menurut saya. Inisialisasi ke 42 tidak secara otomatis berarti bahwa variabelnya adalah int, bisa berupa tipe numerik apa pun termasuk genap char. Saya juga tidak mengerti mengapa Anda ingin mengeja seluruh tipe untuk Entrysaat Anda cukup mengetikkan nama kelas dan biarkan IDE Anda melakukan impor yang diperlukan. Satu-satunya kasus ketika Anda harus mengeja seluruh nama adalah ketika Anda memiliki kelas dengan nama yang sama dalam paket Anda sendiri. Tapi menurut saya desainnya jelek.
Malcolm
10
@ Malcolm Ya, semua contoh saya dibuat-buat. Mereka berfungsi untuk menggambarkan suatu hal. Ketika saya menulis intcontoh, saya berpikir tentang (menurut pendapat saya perilaku yang cukup waras) dari sebagian besar bahasa yang menampilkan inferensi tipe. Mereka biasanya menyimpulkan intatau Integeratau apa pun namanya dalam bahasa itu. Keindahan tipe inferensi adalah bahwa selalu opsional; Anda masih dapat menentukan jenis yang berbeda jika Anda membutuhkannya. Mengenai Entrycontoh: poin bagus, saya akan menggantinya dengan Map.Entry<Integer, Map<Integer, SomeObject<SomeObject, T>>>. Java bahkan tidak memiliki alias alias :(
amon
4
@ m3th0dman Jika jenis ini penting untuk dipahami, maka Anda masih dapat menyebutkannya secara eksplisit. Ketik inferensi selalu opsional. Tapi di sini, jenisnya colKeyjelas dan tidak relevan: kita hanya peduli bahwa itu cocok sebagai argumen kedua doSomethingWith. Jika saya mengekstrak loop itu ke dalam fungsi yang menghasilkan Iterable of- (key1, key2, value)triples, tanda tangan yang paling umum adalah <K1, K2, V> Iterable<TableEntry<K1, K2, V>> flattenTable(Map<K1, Map<K2, V>> table). Di dalam fungsi itu, tipe nyata colKey( Integer, bukan K2) sama sekali tidak relevan.
amon
4
@ m3th0dman itu cukup luas, tentang " sebagian " kode menjadi ini atau itu. Statistik anekdotal. Ada tentu tidak ada gunanya menulis jenis dua kali dalam initializers: View.OnClickListener listener = new View.OnClickListener(). Anda masih akan tahu jenisnya meskipun programmer itu "malas" dan disingkat menjadi var listener = new View.OnClickListener(jika ini mungkin). Jenis redundansi seperti ini biasa terjadi - saya tidak akan mengambil risiko perkiraan di sini - dan menghapusnya memang berasal dari pemikiran tentang pembaca masa depan. Setiap fitur bahasa harus digunakan dengan hati-hati, saya tidak mempertanyakan itu.
Konrad Morawski
26

Benar bahwa kode dibaca jauh lebih sering daripada yang tertulis. Namun, membaca juga membutuhkan waktu, dan dua layar kode lebih sulit untuk dinavigasi dan membaca dari satu layar kode, jadi kita perlu memprioritaskan untuk mengemas rasio informasi-upaya / usaha-membaca yang terbaik. Ini adalah prinsip umum UX: Terlalu banyak informasi sekaligus membanjiri dan sebenarnya menurunkan efektivitas antarmuka.

Dan itu adalah pengalaman saya bahwa sering kali , tipe yang tepat tidak penting. Tentunya Anda kadang-kadang ekspresi sarang: x + y * z, monkey.eat(bananas.get(i)), factory.makeCar().drive(). Masing-masing berisi sub-ekspresi yang mengevaluasi nilai yang jenisnya tidak ditulis. Namun mereka sangat jelas. Kami baik-baik saja dengan membiarkan tipe tidak dinyatakan karena cukup mudah untuk mengetahui dari konteksnya, dan menuliskannya akan lebih berbahaya daripada kebaikan (mengacaukan pemahaman tentang aliran data, mengambil layar yang berharga, dan ruang memori jangka pendek).

Salah satu alasan untuk tidak menyatakan ekspresi seperti tidak ada hari esok adalah bahwa garis menjadi panjang dan aliran nilai menjadi tidak jelas. Memperkenalkan variabel sementara membantu dengan ini, itu memaksakan perintah dan memberi nama untuk hasil parsial. Namun, tidak semua yang mendapat manfaat dari aspek ini juga mendapat manfaat dari jenisnya yang dijabarkan:

user = db.get_poster(request.post['answer'])
name = db.get_display_name(user)

Apakah penting apakah userobjek entitas, integer, string, atau yang lainnya? Untuk sebagian besar tujuan, tidak, itu cukup untuk mengetahui bahwa itu mewakili pengguna, berasal dari permintaan HTTP, dan digunakan untuk mengambil nama untuk ditampilkan di sudut kanan bawah jawaban.

Dan ketika itu tidak peduli, penulis bebas untuk menulis jenis. Ini adalah kebebasan yang harus digunakan secara bertanggung jawab, tetapi hal yang sama berlaku untuk semua hal lain yang dapat meningkatkan keterbacaan (nama variabel dan fungsi, pemformatan, desain API, ruang putih). Dan memang, konvensi dalam Haskell dan ML (di mana semuanya dapat disimpulkan tanpa usaha ekstra) adalah untuk menuliskan jenis fungsi fungsi non-lokal, dan juga variabel lokal dan fungsi kapan pun sesuai. Hanya pemula yang memungkinkan setiap jenis disimpulkan.


sumber
2
+1 Ini harus menjadi jawaban yang diterima. Ini masuk langsung ke jantung mengapa inferensi jenis adalah ide bagus.
Christian Hayter
Jenis persis usertidak masalah jika Anda mencoba untuk memperluas fungsi, karena menentukan apa yang dapat Anda lakukan dengan user. Ini penting jika Anda ingin menambahkan beberapa pemeriksaan kewarasan (karena kerentanan keamanan, misalnya), atau lupa bahwa Anda benar-benar perlu melakukan sesuatu dengan pengguna selain hanya menampilkannya. Benar, jenis bacaan untuk ekspansi ini lebih jarang daripada hanya membaca kode, tetapi juga merupakan bagian penting dari pekerjaan kita.
cmaster
@ cmaster Dan Anda selalu dapat mengetahui jenis itu dengan mudah (sebagian besar IDE akan memberi tahu Anda, dan ada solusi berteknologi rendah yang secara sengaja menyebabkan kesalahan jenis dan membiarkan kompiler mencetak jenis yang sebenarnya), itu hanya tidak memungkinkan sehingga tidak mengganggu Anda dalam kasus umum.
4

Saya pikir inferensi jenis cukup penting dan harus didukung dalam bahasa modern apa pun. Kita semua berkembang dalam IDE dan mereka dapat banyak membantu jika Anda ingin tahu jenis yang disimpulkan, hanya sedikit dari kita yang masuk vi. Pikirkan kode verbositas dan upacara di Jawa misalnya.

  Map<String,HashMap<String,String>> map = getMap();

Tapi Anda bisa mengatakan itu baik-baik saja IDE saya akan membantu saya, itu bisa menjadi poin yang valid. Namun, beberapa fitur tidak akan ada di sana tanpa bantuan tipe inferensi, tipe C # anonim misalnya.

 var person = new {Name="John Smith", Age = 105};

Linq tidak akan sebagus sekarang tanpa bantuan tipe inferensi, Selectmisalnya

  var result = list.Select(c=> new {Name = c.Name.ToUpper(), Age = c.DOB - CurrentDate});

Jenis anonim ini akan disimpulkan dengan rapi ke variabel.

Saya tidak suka ketik inferensi pada jenis kembali Scalakarena saya pikir poin Anda berlaku di sini, harus jelas bagi kami apa fungsi kembali sehingga kami dapat menggunakan API lebih lancar

Sleiman Jneidi
sumber
Map<String,HashMap<String,String>>? Tentu, jika Anda tidak menggunakan jenis, maka mengeja mereka memiliki sedikit manfaat. Table<User, File, String>lebih informatif, dan ada manfaatnya menulisnya.
MikeFHay
4

Saya pikir jawaban untuk ini sangat sederhana: menghemat membaca dan menulis informasi yang berlebihan. Khususnya dalam bahasa berorientasi objek di mana Anda memiliki tipe di kedua sisi tanda sama dengan.

Yang juga memberi tahu Anda kapan Anda harus atau tidak menggunakannya - ketika informasi itu tidak berlebihan.

jmoreno
sumber
3
Nah, secara teknis, informasi selalu berlebihan ketika dimungkinkan untuk menghilangkan tanda tangan manual: jika tidak, kompiler tidak akan dapat menyimpulkannya! Tapi saya mengerti maksud Anda: ketika Anda hanya menduplikasi tanda tangan ke beberapa tempat dalam satu tampilan, itu benar-benar berlebihan bagi otak , sementara beberapa tipe yang ditempatkan dengan baik memberikan informasi yang Anda harus cari sejak lama, mungkin dengan banyak transformasi nonobvious.
leftaroundabout
@leftaroundabout: berlebihan ketika dibaca oleh programmer.
jmoreno
3

Misalkan seseorang melihat kode:

someBigLongGenericType variableName = someBigLongGenericType.someFactoryMethod();

Jika someBigLongGenericTypedapat diberikan dari jenis pengembalian someFactoryMethod, seberapa besar kemungkinan seseorang yang membaca kode untuk memperhatikan jika jenis tidak cocok, dan seberapa mudah seseorang yang melihat perbedaan dapat mengenali apakah itu disengaja atau tidak?

Dengan mengizinkan inferensi, suatu bahasa dapat menyarankan kepada seseorang yang membaca kode bahwa ketika jenis variabel secara eksplisit dinyatakan orang tersebut harus mencoba untuk menemukan alasannya. Ini pada gilirannya memungkinkan orang yang membaca kode untuk lebih memfokuskan upaya mereka. Sebaliknya, jika sebagian besar waktu ketika suatu jenis ditentukan, kebetulan persis sama dengan apa yang telah disimpulkan, maka seseorang yang membaca kode mungkin kurang cenderung memperhatikan waktu bahwa itu agak berbeda. .

supercat
sumber
2

Saya melihat bahwa sudah ada beberapa jawaban yang bagus. Beberapa di antaranya saya akan ulangi tetapi kadang-kadang Anda hanya ingin meletakkan sesuatu dengan kata-kata Anda sendiri. Saya akan berkomentar dengan beberapa contoh dari C ++ karena itu adalah bahasa yang paling saya kenal.

Yang penting tidak pernah tidak bijaksana. Ketik inferensi diperlukan untuk membuat fitur bahasa lainnya praktis. Dalam C ++ dimungkinkan untuk memiliki tipe yang tidak dapat dipisahkan.

struct {
    double x, y;
} p0 = { 0.0, 0.0 };
// there is no name for the type of p0
auto p1 = p0;

C ++ 11 menambahkan lambdas yang juga tidak dapat disangkal.

auto sq = [](int x) {
    return x * x;
};
// there is no name for the type of sq

Ketik inferensi juga mendukung templat.

template <class x_t>
auto sq(x_t const& x)
{
    return x * x;
}
// x_t is not known until it is inferred from an expression
sq(2); // x_t is int
sq(2.0); // x_t is double

Tetapi pertanyaan Anda adalah "mengapa saya, sang programmer, ingin menyimpulkan jenis variabel saya ketika saya membaca kode? Bukankah lebih cepat bagi siapa pun hanya untuk membaca jenisnya daripada memikirkan jenis apa yang ada?"

Ketik inferensi menghilangkan redundansi. Ketika datang untuk membaca kode, kadang-kadang mungkin lebih cepat dan lebih mudah untuk memiliki informasi yang berlebihan dalam kode tetapi redundansi dapat menutupi informasi yang berguna . Sebagai contoh:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Tidak memerlukan banyak keakraban dengan pustaka standar untuk seorang programmer C ++ untuk mengidentifikasi bahwa saya adalah seorang iterator dari i = v.begin()sehingga deklarasi tipe eksplisit memiliki nilai terbatas. Dengan kehadirannya itu mengaburkan detail yang lebih penting (seperti yang imenunjuk ke awal vektor). Jawaban baik oleh @amon memberikan contoh yang lebih baik dari verbosity membayangi detail penting. Sebaliknya menggunakan inferensi tipe memberikan keunggulan lebih besar pada detail penting.

std::vector<int> v;
auto i = v.begin();

Walaupun membaca kode itu penting, itu tidak cukup, pada titik tertentu Anda harus berhenti membaca dan mulai menulis kode baru. Redundansi dalam kode membuat modifikasi kode lebih lambat dan lebih sulit. Misalnya, saya memiliki fragmen kode berikut:

std::vector<int> v;
std::vector<int>::iterator i = v.begin();

Dalam hal ini saya perlu mengubah tipe nilai vektor untuk mengubah kode menjadi dua kali lipat:

std::vector<double> v;
std::vector<double>::iterator i = v.begin();

Dalam hal ini saya harus memodifikasi kode di dua tempat. Kontras dengan inferensi ketik di mana kode aslinya adalah:

std::vector<int> v;
auto i = v.begin();

Dan kode yang dimodifikasi:

std::vector<double> v;
auto i = v.begin();

Perhatikan bahwa sekarang saya hanya perlu mengubah satu baris kode. Ekstrapolasi ini ke program besar dan inferensi tipe dapat menyebarkan perubahan ke tipe jauh lebih cepat daripada yang Anda bisa dengan editor.

Redundansi dalam kode menciptakan kemungkinan bug. Setiap kali kode Anda bergantung pada dua informasi yang dijaga tetap sama, ada kemungkinan kesalahan. Misalnya, ada ketidakkonsistenan antara kedua jenis dalam pernyataan ini yang mungkin tidak dimaksudkan:

int pi = 3.14159;

Redundansi membuat niat lebih sulit untuk dilihat. Dalam beberapa kasus, tipe inferensi dapat lebih mudah dibaca dan dipahami karena lebih sederhana daripada spesifikasi tipe eksplisit. Pertimbangkan fragmen kode:

int y = sq(x);

Dalam kasus yang sq(x)mengembalikan suatu int, tidak jelas apakah ymerupakan intkarena itu adalah jenis pengembalian sq(x)atau karena sesuai dengan pernyataan yang digunakan y. Jika saya mengubah kode lain sehingga sq(x)tidak lagi kembali int, tidak pasti dari baris itu saja apakah jenis yharus diperbarui. Kontras dengan kode yang sama tetapi menggunakan tipe inferensi:

auto y = sq(x);

Dalam hal ini maksudnya jelas, yharus jenis yang sama dengan yang dikembalikan oleh sq(x). Ketika kode mengubah jenis pengembalian sq(x), jenis yperubahan untuk mencocokkan secara otomatis.

Dalam C ++ ada alasan kedua mengapa contoh di atas lebih sederhana dengan inferensi tipe, inferensi tipe tidak dapat memperkenalkan konversi tipe implisit. Jika jenis pengembaliannya sq(x)bukan int, kompiler dengan diam-diam menyisipkan konversi implisit ke int. Jika tipe kembalinya sq(x)adalah tipe kompleks yang operator int()ditentukan, pemanggilan fungsi tersembunyi ini mungkin rumit.

Bowie Owens
sumber
Poin yang cukup bagus tentang tipe-tipe yang tidak dapat diputar dalam C ++. Namun, saya pikir itu bukan alasan untuk menambahkan inferensi tipe, daripada alasan untuk memperbaiki bahasa. Dalam kasus pertama yang Anda sajikan, programmer hanya perlu memberikan nama benda untuk menghindari penggunaan inferensi tipe, jadi ini bukan contoh yang kuat. Contoh kedua hanya kuat karena C ++ secara eksplisit melarang jenis lambda menjadi utterable, bahkan mengetik dengan menggunakan typeoftidak berguna oleh bahasa. Dan itu adalah defisit bahasa itu sendiri yang harus diperbaiki menurut pendapat saya.
cmaster